mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
This commit is contained in:
@@ -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
|
||||
113
.github/workflows/integration-tests.yml
vendored
113
.github/workflows/integration-tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
43
.github/workflows/quick-build.yml
vendored
Normal file
43
.github/workflows/quick-build.yml
vendored
Normal file
@@ -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
|
||||
130
.github/workflows/unit-tests.yml
vendored
130
.github/workflows/unit-tests.yml
vendored
@@ -11,9 +11,9 @@ on:
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
# Call the build workflow to build the solution once
|
||||
# Call the quick-build workflow to build Debug configuration only
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
uses: ./.github/workflows/quick-build.yml
|
||||
|
||||
non_parallel_unittests:
|
||||
name: Non-Parallel Unit Tests
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
needs: build
|
||||
strategy:
|
||||
# Turn off fail-fast to let all runners run even if there are errors
|
||||
fail-fast: true
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15 # Increased from 10 for Windows
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
@@ -40,26 +40,56 @@ jobs:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
name: test-build-artifacts
|
||||
path: .
|
||||
|
||||
# KEEP THIS - It's needed for --no-build to work
|
||||
- name: Restore NuGet packages
|
||||
run: dotnet restore
|
||||
|
||||
# Test
|
||||
# Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
|
||||
# See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
|
||||
# Without it, the test runner crashes on ubuntu (but not Windows or mac)
|
||||
# Optimize Windows performance
|
||||
- name: Disable Windows Defender (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath "${{ github.workspace }}"
|
||||
Add-MpPreference -ExclusionProcess "dotnet.exe"
|
||||
Add-MpPreference -ExclusionProcess "testhost.exe"
|
||||
Add-MpPreference -ExclusionProcess "VSTest.Console.exe"
|
||||
|
||||
- name: Set VSTEST_DUMP_PATH
|
||||
shell: bash
|
||||
run: echo "{VSTEST_DUMP_PATH}={logs/UnitTests/${{ runner.os }}/}" >> $GITHUB_ENV
|
||||
run: echo "VSTEST_DUMP_PATH=logs/UnitTests/${{ runner.os }}/" >> $GITHUB_ENV
|
||||
|
||||
- name: Run UnitTests
|
||||
shell: bash
|
||||
run: |
|
||||
dotnet test Tests/UnitTests --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTests/coverlet.runsettings --diag:logs/UnitTests/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=false
|
||||
|
||||
# mv -v Tests/UnitTests/TestResults/*/*.* TestResults/UnitTests/
|
||||
if [ "${{ runner.os }}" == "Linux" ]; then
|
||||
# Run with coverage on Linux only
|
||||
dotnet test Tests/UnitTests \
|
||||
--no-build \
|
||||
--verbosity normal \
|
||||
--collect:"XPlat Code Coverage" \
|
||||
--settings Tests/UnitTests/runsettings.xml \
|
||||
--diag:logs/UnitTests/${{ runner.os }}/logs.txt \
|
||||
--blame \
|
||||
--blame-crash \
|
||||
--blame-hang \
|
||||
--blame-hang-timeout 60s \
|
||||
--blame-crash-collect-always
|
||||
else
|
||||
# Run without coverage on Windows/macOS for speed
|
||||
dotnet test Tests/UnitTests \
|
||||
--no-build \
|
||||
--verbosity normal \
|
||||
--settings Tests/UnitTests/runsettings.xml \
|
||||
--diag:logs/UnitTests/${{ runner.os }}/logs.txt \
|
||||
--blame \
|
||||
--blame-crash \
|
||||
--blame-hang \
|
||||
--blame-hang-timeout 120s \
|
||||
--blame-crash-collect-always
|
||||
fi
|
||||
|
||||
- name: Upload Test Logs
|
||||
if: always()
|
||||
@@ -68,19 +98,29 @@ jobs:
|
||||
name: non_parallel_unittests-logs-${{ runner.os }}
|
||||
path: |
|
||||
logs/UnitTests
|
||||
TestResults/UnitTests/
|
||||
TestResults/
|
||||
|
||||
- name: Upload Non-Parallel UnitTests Coverage to Codecov
|
||||
if: matrix.os == 'ubuntu-latest' && always()
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: TestResults/**/coverage.cobertura.xml
|
||||
flags: unittests-nonparallel
|
||||
name: UnitTests-${{ runner.os }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
parallel_unittests:
|
||||
name: Parallel Unit Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: build
|
||||
strategy:
|
||||
# Turn off fail-fast to let all runners run even if there are errors
|
||||
fail-fast: true
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
@@ -95,26 +135,54 @@ jobs:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
name: test-build-artifacts
|
||||
path: .
|
||||
|
||||
- name: Restore NuGet packages
|
||||
run: dotnet restore
|
||||
|
||||
# Test
|
||||
# Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
|
||||
# See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
|
||||
# Without it, the test runner crashes on ubuntu (but not Windows or mac)
|
||||
- name: Disable Windows Defender (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath "${{ github.workspace }}"
|
||||
Add-MpPreference -ExclusionProcess "dotnet.exe"
|
||||
Add-MpPreference -ExclusionProcess "testhost.exe"
|
||||
Add-MpPreference -ExclusionProcess "VSTest.Console.exe"
|
||||
|
||||
- name: Set VSTEST_DUMP_PATH
|
||||
shell: bash
|
||||
run: echo "{VSTEST_DUMP_PATH}={logs/UnitTestsParallelizable/${{ runner.os }}/}" >> $GITHUB_ENV
|
||||
run: echo "VSTEST_DUMP_PATH=logs/UnitTestsParallelizable/${{ runner.os }}/" >> $GITHUB_ENV
|
||||
|
||||
- name: Run UnitTestsParallelizable
|
||||
shell: bash
|
||||
run: |
|
||||
dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTestsParallelizable/coverlet.runsettings --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=false
|
||||
|
||||
# mv -v Tests/UnitTestsParallelizable/TestResults/*/*.* TestResults/UnitTestsParallelizable/
|
||||
if [ "${{ runner.os }}" == "Linux" ]; then
|
||||
# Run with coverage on Linux only
|
||||
dotnet test Tests/UnitTestsParallelizable \
|
||||
--no-build \
|
||||
--verbosity normal \
|
||||
--collect:"XPlat Code Coverage" \
|
||||
--settings Tests/UnitTests/runsettings.coverage.xml \
|
||||
--diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \
|
||||
--blame \
|
||||
--blame-crash \
|
||||
--blame-hang \
|
||||
--blame-hang-timeout 60s \
|
||||
--blame-crash-collect-always
|
||||
else
|
||||
# Run without coverage on Windows/macOS for speed
|
||||
dotnet test Tests/UnitTestsParallelizable \
|
||||
--no-build \
|
||||
--verbosity normal \
|
||||
--settings Tests/UnitTestsParallelizable/runsettings.xml \
|
||||
--diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \
|
||||
--blame \
|
||||
--blame-crash \
|
||||
--blame-hang \
|
||||
--blame-hang-timeout 60s \
|
||||
--blame-crash-collect-always
|
||||
fi
|
||||
|
||||
- name: Upload UnitTestsParallelizable Logs
|
||||
if: always()
|
||||
@@ -123,4 +191,14 @@ jobs:
|
||||
name: parallel_unittests-logs-${{ runner.os }}
|
||||
path: |
|
||||
logs/UnitTestsParallelizable/
|
||||
TestResults/UnitTestsParallelizable/
|
||||
TestResults/
|
||||
|
||||
- name: Upload Parallelizable UnitTests Coverage to Codecov
|
||||
if: matrix.os == 'ubuntu-latest' && always()
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: TestResults/**/coverage.cobertura.xml
|
||||
flags: unittests-parallel
|
||||
name: UnitTestsParallelizable-${{ runner.os }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -68,3 +68,8 @@ BenchmarkDotNet.Artifacts/
|
||||
*.log.*
|
||||
|
||||
log.*
|
||||
|
||||
/Tests/coverage/
|
||||
!/Tests/coverage/.gitkeep # keep folder in repo
|
||||
/Tests/report/
|
||||
*.cobertura.xml
|
||||
|
||||
107
CONTRIBUTING.md
107
CONTRIBUTING.md
@@ -23,11 +23,10 @@ Welcome! This guide provides everything you need to know to contribute effective
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files, 333MB) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system.
|
||||
**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system.
|
||||
|
||||
**Key characteristics:**
|
||||
- **Language**: C# (net8.0)
|
||||
- **Size**: ~496 source files in core library, ~1,050 total C# files
|
||||
- **Platform**: Cross-platform (Windows, macOS, Linux)
|
||||
- **Architecture**: Console UI toolkit with driver-based architecture
|
||||
- **Version**: v2 (Alpha), v1 (maintenance mode)
|
||||
@@ -88,25 +87,12 @@ Welcome! This guide provides everything you need to know to contribute effective
|
||||
dotnet test Tests/IntegrationTests --no-build --verbosity normal
|
||||
```
|
||||
|
||||
**Important**: Tests may take significant time. CI uses blame flags for crash detection:
|
||||
```bash
|
||||
--diag:logs/UnitTests/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always
|
||||
```
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
#### Issue: Build Warnings
|
||||
- **Expected**: ~326 warnings (nullable refs, unused vars, xUnit suggestions)
|
||||
- **Expected**: None warnings (~100 currently).
|
||||
- **Action**: Don't add new warnings; fix warnings in code you modify
|
||||
|
||||
#### Issue: Test Timeouts
|
||||
- **Expected**: Tests can take 5-10 minutes
|
||||
- **Action**: Use appropriate timeout values (60-120 seconds for test commands)
|
||||
|
||||
#### Issue: Restore Failures
|
||||
- **Solution**: Ensure `dotnet restore` completes before building
|
||||
- **Note**: Takes 15-20 seconds on first run
|
||||
|
||||
#### Issue: NativeAot/SelfContained Build
|
||||
- **Solution**: Restore these projects explicitly:
|
||||
```bash
|
||||
@@ -135,19 +121,18 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
|
||||
|
||||
### Code Formatting
|
||||
|
||||
**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code:**
|
||||
|
||||
- **Do NOT add formatting tools** - Use existing `.editorconfig` and `Terminal.sln.DotSettings`
|
||||
- Format code with:
|
||||
1. ReSharper/Rider (`Ctrl-E-C`)
|
||||
2. JetBrains CleanupCode CLI tool (free)
|
||||
3. Visual Studio (`Ctrl-K-D`) as fallback
|
||||
- **Only format files you modify**
|
||||
|
||||
### Critical Coding Rules
|
||||
|
||||
**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code:**
|
||||
|
||||
#### Type Declarations and Object Creation
|
||||
|
||||
- Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords)
|
||||
- 4-space indentation
|
||||
- No trailing whitespace
|
||||
- File-scoped namespaces
|
||||
- **ALWAYS use explicit types** - Never use `var` except for built-in simple types (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`)
|
||||
```csharp
|
||||
// ✅ CORRECT - Explicit types
|
||||
@@ -163,7 +148,7 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
|
||||
var views = new List<View?>();
|
||||
```
|
||||
|
||||
- **ALWAYS use target-typed `new()`** - Use `new ()` instead of `new TypeName()` when the type is already declared
|
||||
- **ALWAYS use target-typed `new ()`** - Use `new ()` instead of `new TypeName()` when the type is already declared
|
||||
```csharp
|
||||
// ✅ CORRECT - Target-typed new
|
||||
View view = new () { Width = 10 };
|
||||
@@ -174,13 +159,6 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
|
||||
MouseEventArgs args = new MouseEventArgs();
|
||||
```
|
||||
|
||||
#### Other Conventions
|
||||
|
||||
- Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords)
|
||||
- 4-space indentation
|
||||
- No trailing whitespace
|
||||
- File-scoped namespaces
|
||||
|
||||
**⚠️ CRITICAL - These conventions apply to ALL code - production code, test code, examples, and samples.**
|
||||
|
||||
---
|
||||
@@ -191,6 +169,11 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
|
||||
|
||||
- **Never decrease code coverage** - PRs must maintain or increase coverage
|
||||
- Target: 70%+ coverage for new code
|
||||
- **Coverage collection**:
|
||||
- Centralized in `TestResults/` directory at repository root
|
||||
- Collected only on Linux (ubuntu-latest) runners in CI for performance
|
||||
- Windows and macOS runners skip coverage collection to reduce execution time
|
||||
- Coverage reports uploaded to Codecov automatically from Linux runner
|
||||
- CI monitors coverage on each PR
|
||||
|
||||
### Test Patterns
|
||||
@@ -258,33 +241,58 @@ The repository uses multiple GitHub Actions workflows. What runs and when:
|
||||
- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`); supports `workflow_call`
|
||||
- **Runner/timeout**: `ubuntu-latest`, 10 minutes
|
||||
- **Steps**:
|
||||
- Checkout and setup .NET 8.x GA
|
||||
- `dotnet restore`
|
||||
- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612`
|
||||
- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612`
|
||||
- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612`
|
||||
- Restore NativeAot/SelfContained examples, then restore solution again
|
||||
- Build Release for `Examples/NativeAot` and `Examples/SelfContained`
|
||||
- Build Release solution
|
||||
- Upload artifacts named `build-artifacts`, retention 1 day
|
||||
- Checkout and setup .NET 8.x GA
|
||||
- `dotnet restore`
|
||||
- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612`
|
||||
- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612`
|
||||
- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612`
|
||||
- Restore NativeAot/SelfContained examples, then restore solution again
|
||||
- Build Release for `Examples/NativeAot` and `Examples/SelfContained`
|
||||
- Build Release solution
|
||||
- Upload artifacts named `build-artifacts`, retention 1 day
|
||||
|
||||
### 2) Build & Run Unit Tests (`.github/workflows/unit-tests.yml`)
|
||||
|
||||
- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`)
|
||||
- **Process**: Calls build workflow, then runs:
|
||||
- Non-parallel UnitTests on Ubuntu/Windows/macOS matrix with coverage and blame/diag flags; `xunit.stopOnFail=false`
|
||||
- Parallel UnitTestsParallelizable similarly with coverage; `xunit.stopOnFail=false`
|
||||
- Uploads logs per-OS
|
||||
- **Matrix**: Ubuntu/Windows/macOS
|
||||
- **Timeout**: 15 minutes per job
|
||||
- **Process**:
|
||||
1. Calls build workflow to build solution once
|
||||
2. Downloads build artifacts
|
||||
3. Runs `dotnet restore` (required for `--no-build` to work)
|
||||
4. **Performance optimizations**:
|
||||
- Disables Windows Defender on Windows runners (significant speedup)
|
||||
- Collects code coverage **only on Linux** (ubuntu-latest) for performance
|
||||
- Windows and macOS skip coverage collection to reduce test time
|
||||
- Increased blame-hang-timeout to 120s for Windows/macOS (60s for Linux)
|
||||
5. Runs two test jobs:
|
||||
- **Non-parallel UnitTests**: `Tests/UnitTests` with blame/diag flags; `xunit.stopOnFail=false`
|
||||
- **Parallel UnitTestsParallelizable**: `Tests/UnitTestsParallelizable` with blame/diag flags; `xunit.stopOnFail=false`
|
||||
6. Uploads test logs and diagnostic data from all runners
|
||||
7. **Uploads code coverage to Codecov only from Linux runner**
|
||||
|
||||
**Test results**: All tests output to unified `TestResults/` directory at repository root
|
||||
|
||||
### 3) Build & Run Integration Tests (`.github/workflows/integration-tests.yml`)
|
||||
|
||||
- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`)
|
||||
- **Process**: Calls build workflow, then runs IntegrationTests on matrix with blame/diag; `xunit.stopOnFail=true`
|
||||
- Uploads logs per-OS
|
||||
- **Matrix**: Ubuntu/Windows/macOS
|
||||
- **Timeout**: 15 minutes
|
||||
- **Process**:
|
||||
1. Calls build workflow
|
||||
2. Downloads build artifacts
|
||||
3. Runs `dotnet restore`
|
||||
4. **Performance optimizations** (same as unit tests):
|
||||
- Disables Windows Defender on Windows runners
|
||||
- Collects code coverage **only on Linux**
|
||||
- Increased blame-hang-timeout to 120s for Windows/macOS
|
||||
5. Runs IntegrationTests with blame/diag flags; `xunit.stopOnFail=true`
|
||||
6. Uploads logs per-OS
|
||||
7. **Uploads coverage to Codecov only from Linux runner**
|
||||
|
||||
### 4) Publish to NuGet (`.github/workflows/publish.yml`)
|
||||
|
||||
- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*` (ignores `**.md`)
|
||||
- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*`(ignores `**.md`)
|
||||
- Uses GitVersion to compute SemVer, builds Release, packs with symbols, and pushes to NuGet.org using `NUGET_API_KEY`
|
||||
|
||||
### 5) Build and publish API docs (`.github/workflows/api-docs.yml`)
|
||||
@@ -292,6 +300,7 @@ The repository uses multiple GitHub Actions workflows. What runs and when:
|
||||
- **Triggers**: push to `v1_release` and `v2_develop`
|
||||
- Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `v2_release` or `v2_develop`
|
||||
|
||||
|
||||
### Replicating CI Locally
|
||||
|
||||
```bash
|
||||
@@ -323,9 +332,9 @@ dotnet build --configuration Release --no-restore
|
||||
### Main Directories
|
||||
|
||||
**`/Terminal.Gui/`** - Core library (496 C# files):
|
||||
- `App/` - Application lifecycle (`Application.cs` static class, `RunState`, `MainLoop`)
|
||||
- `App/` - Application lifecycle (`Application.cs` static class, `SessionToken`, `MainLoop`)
|
||||
- `Configuration/` - `ConfigurationManager` for settings
|
||||
- `Drivers/` - Console driver implementations (`IConsoleDriver`, `NetDriver`, `UnixDriver`, `WindowsDriver`)
|
||||
- `Drivers/` - Console driver implementations (`Dotnet`, `Windows`, `Unix`, `Fake`)
|
||||
- `Drawing/` - Rendering system (attributes, colors, glyphs)
|
||||
- `Input/` - Keyboard and mouse input handling
|
||||
- `ViewBase/` - Core `View` class hierarchy and layout
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace SelfContained;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
[RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, IConsoleDriver)")]
|
||||
[RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, IDriver)")]
|
||||
private static void Main (string [] args)
|
||||
{
|
||||
ConfigurationManager.Enable (ConfigLocations.All);
|
||||
|
||||
@@ -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!);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -129,7 +129,7 @@ public class Mazing : Scenario
|
||||
return;
|
||||
}
|
||||
|
||||
Point newPos = _m.Player;
|
||||
Point newPos = _m!.Player;
|
||||
|
||||
Command? command = e.Context?.Command;
|
||||
|
||||
|
||||
@@ -107,18 +107,7 @@ public class Navigation : Scenario
|
||||
// };
|
||||
//timer.Start ();
|
||||
|
||||
Application.Iteration += (sender, args) =>
|
||||
{
|
||||
if (progressBar.Fraction == 1.0)
|
||||
{
|
||||
progressBar.Fraction = 0;
|
||||
}
|
||||
|
||||
progressBar.Fraction += 0.01f;
|
||||
|
||||
Application.Invoke (() => { });
|
||||
|
||||
};
|
||||
Application.Iteration += OnApplicationIteration;
|
||||
|
||||
View overlappedView2 = CreateOverlappedView (3, 8, 10);
|
||||
|
||||
@@ -214,12 +203,25 @@ public class Navigation : Scenario
|
||||
|
||||
testFrame.SetFocus ();
|
||||
Application.Run (app);
|
||||
Application.Iteration -= OnApplicationIteration;
|
||||
// timer.Close ();
|
||||
app.Dispose ();
|
||||
Application.Shutdown ();
|
||||
|
||||
return;
|
||||
|
||||
void OnApplicationIteration (object sender, IterationEventArgs args)
|
||||
{
|
||||
if (progressBar.Fraction == 1.0)
|
||||
{
|
||||
progressBar.Fraction = 0;
|
||||
}
|
||||
|
||||
progressBar.Fraction += 0.01f;
|
||||
|
||||
Application.Invoke (() => { });
|
||||
}
|
||||
|
||||
void ColorPicker_ColorChanged (object sender, ResultEventArgs<Color> e)
|
||||
{
|
||||
testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) });
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
[ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")]
|
||||
@@ -9,10 +7,10 @@ namespace UICatalog.Scenarios;
|
||||
[ScenarioCategory ("TextView")]
|
||||
public class Notepad : Scenario
|
||||
{
|
||||
private TabView _focusedTabView;
|
||||
public Shortcut LenShortcut { get; private set; }
|
||||
private TabView? _focusedTabView;
|
||||
private int _numNewTabs = 1;
|
||||
private TabView _tabView;
|
||||
private TabView? _tabView;
|
||||
public Shortcut? LenShortcut { get; private set; }
|
||||
|
||||
public override void Main ()
|
||||
{
|
||||
@@ -67,14 +65,15 @@ public class Notepad : Scenario
|
||||
top.Add (_tabView);
|
||||
LenShortcut = new (Key.Empty, "Len: ", null);
|
||||
|
||||
var statusBar = new StatusBar (new [] {
|
||||
new (Application.QuitKey, $"Quit", Quit),
|
||||
new Shortcut(Key.F2, "Open", Open),
|
||||
new Shortcut(Key.F1, "New", New),
|
||||
var statusBar = new StatusBar (
|
||||
[
|
||||
new (Application.QuitKey, "Quit", Quit),
|
||||
new (Key.F2, "Open", Open),
|
||||
new (Key.F1, "New", New),
|
||||
new (Key.F3, "Save", Save),
|
||||
new (Key.F6, "Close", Close),
|
||||
LenShortcut
|
||||
}
|
||||
]
|
||||
)
|
||||
{
|
||||
AlignmentModes = AlignmentModes.IgnoreFirstOrLast
|
||||
@@ -97,7 +96,7 @@ public class Notepad : Scenario
|
||||
Application.Shutdown ();
|
||||
}
|
||||
|
||||
public void Save () { Save (_focusedTabView, _focusedTabView.SelectedTab); }
|
||||
public void Save () { Save (_focusedTabView!, _focusedTabView!.SelectedTab!); }
|
||||
|
||||
public void Save (TabView tabViewToSave, Tab tabToSave)
|
||||
{
|
||||
@@ -119,7 +118,7 @@ public class Notepad : Scenario
|
||||
|
||||
public bool SaveAs ()
|
||||
{
|
||||
var tab = _focusedTabView.SelectedTab as OpenedFile;
|
||||
var tab = _focusedTabView!.SelectedTab as OpenedFile;
|
||||
|
||||
if (tab == null)
|
||||
{
|
||||
@@ -152,7 +151,7 @@ public class Notepad : Scenario
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Close () { Close (_focusedTabView, _focusedTabView.SelectedTab); }
|
||||
private void Close () { Close (_focusedTabView!, _focusedTabView!.SelectedTab!); }
|
||||
|
||||
private void Close (TabView tv, Tab tabToClose)
|
||||
{
|
||||
@@ -196,7 +195,7 @@ public class Notepad : Scenario
|
||||
|
||||
// close and dispose the tab
|
||||
tv.RemoveTab (tab);
|
||||
tab.View.Dispose ();
|
||||
tab.View?.Dispose ();
|
||||
_focusedTabView = tv;
|
||||
|
||||
// If last tab is closed, open a new one
|
||||
@@ -217,7 +216,7 @@ public class Notepad : Scenario
|
||||
return tv;
|
||||
}
|
||||
|
||||
private void New () { Open (null, $"new {_numNewTabs++}"); }
|
||||
private void New () { Open (null!, $"new {_numNewTabs++}"); }
|
||||
|
||||
private void Open ()
|
||||
{
|
||||
@@ -246,26 +245,27 @@ public class Notepad : Scenario
|
||||
|
||||
/// <summary>Creates a new tab with initial text</summary>
|
||||
/// <param name="fileInfo">File that was read or null if a new blank document</param>
|
||||
/// <param name="tabName"></param>
|
||||
private void Open (FileInfo fileInfo, string tabName)
|
||||
{
|
||||
var tab = new OpenedFile (this) { DisplayText = tabName, File = fileInfo };
|
||||
tab.View = tab.CreateTextView (fileInfo);
|
||||
tab.SavedText = tab.View.Text;
|
||||
tab.RegisterTextViewEvents (_focusedTabView);
|
||||
tab.RegisterTextViewEvents (_focusedTabView!);
|
||||
|
||||
_focusedTabView.AddTab (tab, true);
|
||||
_focusedTabView!.AddTab (tab, true);
|
||||
}
|
||||
|
||||
private void Quit () { Application.RequestStop (); }
|
||||
|
||||
private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e)
|
||||
private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e)
|
||||
{
|
||||
LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
|
||||
LenShortcut!.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
|
||||
|
||||
e.NewTab?.View?.SetFocus ();
|
||||
}
|
||||
|
||||
private void TabView_TabClicked (object sender, TabMouseEventArgs e)
|
||||
private void TabView_TabClicked (object? sender, TabMouseEventArgs e)
|
||||
{
|
||||
// we are only interested in right clicks
|
||||
if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked))
|
||||
@@ -281,12 +281,12 @@ public class Notepad : Scenario
|
||||
}
|
||||
else
|
||||
{
|
||||
var tv = (TabView)sender;
|
||||
var tv = (TabView)sender!;
|
||||
var t = (OpenedFile)e.Tab;
|
||||
|
||||
items =
|
||||
[
|
||||
new MenuItemv2 ("Save", "", () => Save (_focusedTabView, e.Tab)),
|
||||
new MenuItemv2 ("Save", "", () => Save (_focusedTabView!, e.Tab)),
|
||||
new MenuItemv2 ("Close", "", () => Close (tv, e.Tab))
|
||||
];
|
||||
|
||||
@@ -303,12 +303,12 @@ public class Notepad : Scenario
|
||||
|
||||
private class OpenedFile (Notepad notepad) : Tab
|
||||
{
|
||||
private Notepad _notepad = notepad;
|
||||
private readonly Notepad _notepad = notepad;
|
||||
|
||||
public OpenedFile CloneTo (TabView other)
|
||||
{
|
||||
var newTab = new OpenedFile (_notepad) { DisplayText = base.Text, File = File };
|
||||
newTab.View = newTab.CreateTextView (newTab.File);
|
||||
newTab.View = newTab.CreateTextView (newTab.File!);
|
||||
newTab.SavedText = newTab.View.Text;
|
||||
newTab.RegisterTextViewEvents (other);
|
||||
other.AddTab (newTab, true);
|
||||
@@ -316,11 +316,11 @@ public class Notepad : Scenario
|
||||
return newTab;
|
||||
}
|
||||
|
||||
public View CreateTextView (FileInfo file)
|
||||
public View CreateTextView (FileInfo? file)
|
||||
{
|
||||
var initialText = string.Empty;
|
||||
|
||||
if (file != null && file.Exists)
|
||||
if (file is { Exists: true })
|
||||
{
|
||||
initialText = System.IO.File.ReadAllText (file.FullName);
|
||||
}
|
||||
@@ -336,11 +336,11 @@ public class Notepad : Scenario
|
||||
};
|
||||
}
|
||||
|
||||
public FileInfo File { get; set; }
|
||||
public FileInfo? File { get; set; }
|
||||
|
||||
public void RegisterTextViewEvents (TabView parent)
|
||||
{
|
||||
var textView = (TextView)View;
|
||||
var textView = (TextView)View!;
|
||||
|
||||
// when user makes changes rename tab to indicate unsaved
|
||||
textView.ContentsChanged += (s, k) =>
|
||||
@@ -362,19 +362,20 @@ public class Notepad : Scenario
|
||||
DisplayText = Text.TrimEnd ('*');
|
||||
}
|
||||
}
|
||||
_notepad.LenShortcut.Title = $"Len:{textView.Text.Length}";
|
||||
|
||||
_notepad.LenShortcut!.Title = $"Len:{textView.Text.Length}";
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>The text of the tab the last time it was saved</summary>
|
||||
/// <value></value>
|
||||
public string SavedText { get; set; }
|
||||
public string? SavedText { get; set; }
|
||||
|
||||
public bool UnsavedChanges => !string.Equals (SavedText, View.Text);
|
||||
public bool UnsavedChanges => !string.Equals (SavedText, View!.Text);
|
||||
|
||||
internal void Save ()
|
||||
{
|
||||
string newText = View.Text;
|
||||
string newText = View!.Text;
|
||||
|
||||
if (File is null || string.IsNullOrWhiteSpace (File.FullName))
|
||||
{
|
||||
|
||||
@@ -266,14 +266,14 @@ internal class NumericUpDownEditor<T> : View where T : notnull
|
||||
|
||||
void NumericUpDownOnIncrementChanged (object? o, EventArgs<T> eventArgs)
|
||||
{
|
||||
_increment.Text = _numericUpDown.Increment.ToString ();
|
||||
_increment.Text = _numericUpDown!.Increment?.ToString ();
|
||||
}
|
||||
|
||||
Add (_numericUpDown);
|
||||
|
||||
_value.Text = _numericUpDown.Text;
|
||||
_format.Text = _numericUpDown.Format;
|
||||
_increment.Text = _numericUpDown.Increment.ToString ();
|
||||
_increment.Text = _numericUpDown!.Increment?.ToString ();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -498,5 +498,5 @@ public class TextInputControls : Scenario
|
||||
|
||||
|
||||
|
||||
private void TimeChanged (object sender, DateTimeEventArgs<TimeSpan> e) { _labelMirroringTimeField.Text = _timeField.Text; }
|
||||
private void TimeChanged (object sender, EventArgs<TimeSpan> e) { _labelMirroringTimeField.Text = _timeField.Text; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
@@ -7,12 +8,12 @@ namespace UICatalog.Scenarios;
|
||||
[ScenarioCategory ("DateTime")]
|
||||
public class TimeAndDate : Scenario
|
||||
{
|
||||
private Label _lblDateFmt;
|
||||
private Label _lblNewDate;
|
||||
private Label _lblNewTime;
|
||||
private Label _lblOldDate;
|
||||
private Label _lblOldTime;
|
||||
private Label _lblTimeFmt;
|
||||
private Label? _lblDateFmt;
|
||||
private Label? _lblNewDate;
|
||||
private Label? _lblNewTime;
|
||||
private Label? _lblOldDate;
|
||||
private Label? _lblOldTime;
|
||||
private Label? _lblTimeFmt;
|
||||
|
||||
public override void Main ()
|
||||
{
|
||||
@@ -143,17 +144,13 @@ public class TimeAndDate : Scenario
|
||||
Application.Shutdown ();
|
||||
}
|
||||
|
||||
private void DateChanged (object sender, DateTimeEventArgs<DateTime> e)
|
||||
private void DateChanged (object? sender, EventArgs<DateTime> e)
|
||||
{
|
||||
_lblOldDate.Text = $"Old Date: {e.OldValue}";
|
||||
_lblNewDate.Text = $"New Date: {e.NewValue}";
|
||||
_lblDateFmt.Text = $"Date Format: {e.Format}";
|
||||
_lblNewDate!.Text = $"New Date: {e.Value}";
|
||||
}
|
||||
|
||||
private void TimeChanged (object sender, DateTimeEventArgs<TimeSpan> e)
|
||||
private void TimeChanged (object? sender, EventArgs<TimeSpan> e)
|
||||
{
|
||||
_lblOldTime.Text = $"Old Time: {e.OldValue}";
|
||||
_lblNewTime.Text = $"New Time: {e.NewValue}";
|
||||
_lblTimeFmt.Text = $"Time Format: {e.Format}";
|
||||
_lblNewTime!.Text = $"New Time: {e.Value}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -71,8 +71,7 @@ public class UnicodeInMenu : Scenario
|
||||
appWindow.Add (menu);
|
||||
|
||||
var statusBar = new StatusBar (
|
||||
new Shortcut []
|
||||
{
|
||||
[
|
||||
new (
|
||||
Application.QuitKey,
|
||||
"Выход",
|
||||
@@ -80,7 +79,7 @@ public class UnicodeInMenu : Scenario
|
||||
),
|
||||
new (Key.F2, "Создать", null),
|
||||
new (Key.F3, "Со_хранить", null)
|
||||
}
|
||||
]
|
||||
);
|
||||
appWindow.Add (statusBar);
|
||||
|
||||
@@ -145,13 +144,13 @@ public class UnicodeInMenu : Scenario
|
||||
};
|
||||
appWindow.Add (checkBox, checkBoxRight);
|
||||
|
||||
label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" };
|
||||
appWindow.Add (label);
|
||||
var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
|
||||
comboBox.SetSource (new ObservableCollection<string> { gitString, "Со_хранить" });
|
||||
//label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" };
|
||||
//appWindow.Add (label);
|
||||
//var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
|
||||
//comboBox.SetSource (new ObservableCollection<string> { gitString, "Со_хранить" });
|
||||
|
||||
appWindow.Add (comboBox);
|
||||
comboBox.Text = gitString;
|
||||
//appWindow.Add (comboBox);
|
||||
//comboBox.Text = gitString;
|
||||
|
||||
label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 2, Text = "HexView:" };
|
||||
appWindow.Add (label);
|
||||
@@ -185,7 +184,7 @@ public class UnicodeInMenu : Scenario
|
||||
X = 20,
|
||||
Y = Pos.Y (label),
|
||||
Width = Dim.Percent (60),
|
||||
RadioLabels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }
|
||||
RadioLabels = ["item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ"]
|
||||
};
|
||||
appWindow.Add (radioGroup);
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ public class UICatalog
|
||||
// Get allowed driver names
|
||||
string? [] allowedDrivers = Application.GetDriverTypes ().Item2.ToArray ();
|
||||
|
||||
Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.")
|
||||
Option<string> driverOption = new Option<string> ("--driver", "The IDriver to use.")
|
||||
.FromAmong (allowedDrivers!);
|
||||
driverOption.SetDefaultValue (string.Empty);
|
||||
driverOption.AddAlias ("-d");
|
||||
@@ -635,7 +635,7 @@ public class UICatalog
|
||||
if (!View.EnableDebugIDisposableAsserts)
|
||||
{
|
||||
View.Instances.Clear ();
|
||||
RunState.Instances.Clear ();
|
||||
SessionToken.Instances.Clear ();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -650,15 +650,15 @@ public class UICatalog
|
||||
|
||||
View.Instances.Clear ();
|
||||
|
||||
// Validate there are no outstanding Application.RunState-based instances
|
||||
// Validate there are no outstanding Application sessions
|
||||
// after a scenario was selected to run. This proves the main UI Catalog
|
||||
// 'app' closed cleanly.
|
||||
foreach (RunState? inst in RunState.Instances)
|
||||
foreach (SessionToken? inst in SessionToken.Instances)
|
||||
{
|
||||
Debug.Assert (inst.WasDisposed);
|
||||
}
|
||||
|
||||
RunState.Instances.Clear ();
|
||||
SessionToken.Instances.Clear ();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
[](https://www.nuget.org/packages/Terminal.Gui)
|
||||

|
||||
[](https://codecov.io/gh/gui-cs/Terminal.Gui)
|
||||
[](https://www.nuget.org/packages/Terminal.Gui)
|
||||
[](LICENSE)
|
||||

|
||||
|
||||
109
Scripts/Run-LocalCoverage.ps1
Normal file
109
Scripts/Run-LocalCoverage.ps1
Normal file
@@ -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"
|
||||
@@ -17,10 +17,10 @@ using JetBrains.Annotations;
|
||||
|
||||
public sealed class ProjectBuilder
|
||||
{
|
||||
private string _sourceCode;
|
||||
private string _expectedFixedCode;
|
||||
private DiagnosticAnalyzer _analyzer;
|
||||
private CodeFixProvider _codeFix;
|
||||
private string? _sourceCode;
|
||||
private string? _expectedFixedCode;
|
||||
private DiagnosticAnalyzer? _analyzer;
|
||||
private CodeFixProvider? _codeFix;
|
||||
|
||||
public ProjectBuilder WithSourceCode (string source)
|
||||
{
|
||||
@@ -59,20 +59,22 @@ public sealed class ProjectBuilder
|
||||
}
|
||||
|
||||
// Parse original document
|
||||
var document = CreateDocument (_sourceCode);
|
||||
var compilation = await document.Project.GetCompilationAsync ();
|
||||
Document document = CreateDocument (_sourceCode);
|
||||
Compilation? compilation = await document.Project.GetCompilationAsync ();
|
||||
|
||||
var diagnostics = compilation.GetDiagnostics ();
|
||||
var errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
|
||||
ImmutableArray<Diagnostic> diagnostics = compilation!.GetDiagnostics ();
|
||||
IEnumerable<Diagnostic> errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
|
||||
|
||||
if (errors.Any ())
|
||||
IEnumerable<Diagnostic> enumerable = errors as Diagnostic [] ?? errors.ToArray ();
|
||||
|
||||
if (enumerable.Any ())
|
||||
{
|
||||
var errorMessages = string.Join (Environment.NewLine, errors.Select (e => e.ToString ()));
|
||||
string errorMessages = string.Join (Environment.NewLine, enumerable.Select (e => e.ToString ()));
|
||||
throw new Exception ("Compilation failed with errors:" + Environment.NewLine + errorMessages);
|
||||
}
|
||||
|
||||
// Run analyzer
|
||||
var analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
|
||||
ImmutableArray<Diagnostic> analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
|
||||
|
||||
Assert.NotEmpty (analyzerDiagnostics);
|
||||
|
||||
@@ -83,83 +85,87 @@ public sealed class ProjectBuilder
|
||||
throw new InvalidOperationException ("Expected code fix but none was set.");
|
||||
}
|
||||
|
||||
var fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
|
||||
Document? fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
|
||||
|
||||
var formattedDocument = await Formatter.FormatAsync (fixedDocument);
|
||||
var fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
|
||||
if (fixedDocument is { })
|
||||
{
|
||||
Document formattedDocument = await Formatter.FormatAsync (fixedDocument);
|
||||
string fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
|
||||
|
||||
Assert.Equal (_expectedFixedCode, fixedSource);
|
||||
Assert.Equal (_expectedFixedCode, fixedSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Document CreateDocument (string source)
|
||||
{
|
||||
var dd = typeof (Enumerable).GetTypeInfo ().Assembly.Location;
|
||||
var coreDir = Directory.GetParent (dd) ?? throw new Exception ($"Could not find parent directory of dotnet sdk. Sdk directory was {dd}");
|
||||
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> ()
|
||||
{
|
||||
MetadataReference.CreateFromFile(typeof(Button).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(View).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(System.IO.FileSystemInfo).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(MarshalByValueComponent).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(ObservableCollection<string>).Assembly.Location),
|
||||
List<MetadataReference> references =
|
||||
[
|
||||
MetadataReference.CreateFromFile (typeof (Button).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (View).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (System.IO.FileSystemInfo).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (System.Linq.Enumerable).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (MarshalByValueComponent).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (ObservableCollection<string>).Assembly.Location),
|
||||
|
||||
// New assemblies required by Terminal.Gui version 2
|
||||
MetadataReference.CreateFromFile(typeof(Size).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(CanBeNullAttribute).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (Size).Assembly.Location),
|
||||
MetadataReference.CreateFromFile (typeof (CanBeNullAttribute).Assembly.Location),
|
||||
|
||||
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "mscorlib.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Runtime.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Collections.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Data.Common.dll")),
|
||||
MetadataReference.CreateFromFile (Path.Combine (coreDir.FullName, "mscorlib.dll")),
|
||||
MetadataReference.CreateFromFile (Path.Combine (coreDir.FullName, "System.Runtime.dll")),
|
||||
MetadataReference.CreateFromFile (Path.Combine (coreDir.FullName, "System.Collections.dll")),
|
||||
MetadataReference.CreateFromFile (Path.Combine (coreDir.FullName, "System.Data.Common.dll"))
|
||||
|
||||
// Add more as necessary
|
||||
};
|
||||
];
|
||||
|
||||
|
||||
var projectInfo = ProjectInfo.Create (
|
||||
projectId,
|
||||
VersionStamp.Create (),
|
||||
"TestProject",
|
||||
"TestAssembly",
|
||||
LanguageNames.CSharp,
|
||||
compilationOptions: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary),
|
||||
metadataReferences: references);
|
||||
ProjectInfo projectInfo = ProjectInfo.Create (
|
||||
projectId,
|
||||
VersionStamp.Create (),
|
||||
"TestProject",
|
||||
"TestAssembly",
|
||||
LanguageNames.CSharp,
|
||||
compilationOptions: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary),
|
||||
metadataReferences: references);
|
||||
|
||||
var solution = workspace.CurrentSolution
|
||||
.AddProject (projectInfo)
|
||||
.AddDocument (documentId, "Test.cs", SourceText.From (source));
|
||||
Solution solution = workspace.CurrentSolution
|
||||
.AddProject (projectInfo)
|
||||
.AddDocument (documentId, "Test.cs", SourceText.From (source));
|
||||
|
||||
return solution.GetDocument (documentId)!;
|
||||
}
|
||||
|
||||
private static async Task<ImmutableArray<Diagnostic>> GetAnalyzerDiagnosticsAsync (Compilation compilation, DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
var compilationWithAnalyzers = compilation.WithAnalyzers (ImmutableArray.Create (analyzer));
|
||||
CompilationWithAnalyzers compilationWithAnalyzers = compilation.WithAnalyzers ([analyzer]);
|
||||
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync ();
|
||||
}
|
||||
|
||||
private static async Task<Document> ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix)
|
||||
private static async Task<Document?> ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix)
|
||||
{
|
||||
CodeAction _codeAction = null;
|
||||
var context = new CodeFixContext ((TextDocument)document, diagnostic, (action, _) => _codeAction = action, CancellationToken.None);
|
||||
CodeAction? codeAction = null;
|
||||
var context = new CodeFixContext ((TextDocument)document, diagnostic, (action, _) => codeAction = action, CancellationToken.None);
|
||||
|
||||
await codeFix.RegisterCodeFixesAsync (context);
|
||||
|
||||
if (_codeAction == null)
|
||||
if (codeAction == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Code fix did not register a fix.");
|
||||
}
|
||||
|
||||
var operations = await _codeAction.GetOperationsAsync (CancellationToken.None);
|
||||
var solution = operations.OfType<ApplyChangesOperation> ().First ().ChangedSolution;
|
||||
ImmutableArray<CodeActionOperation> operations = await codeAction.GetOperationsAsync (CancellationToken.None);
|
||||
Solution solution = operations.OfType<ApplyChangesOperation> ().First ().ChangedSolution;
|
||||
return solution.GetDocument (document.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public static partial class Application // Driver abstractions
|
||||
{
|
||||
internal static bool _forceFakeConsole;
|
||||
|
||||
/// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
|
||||
public static IConsoleDriver? Driver
|
||||
/// <inheritdoc cref="IApplication.Driver"/>
|
||||
public static IDriver? Driver
|
||||
{
|
||||
get => ApplicationImpl.Instance.Driver;
|
||||
internal set => ApplicationImpl.Instance.Driver = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in
|
||||
/// <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
|
||||
/// as long as the selected <see cref="IConsoleDriver"/> supports TrueColor.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IApplication.Force16Colors"/>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static bool Force16Colors
|
||||
{
|
||||
@@ -25,14 +21,7 @@ public static partial class Application // Driver abstractions
|
||||
set => ApplicationImpl.Instance.Force16Colors = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not
|
||||
/// specified, the driver is selected based on the platform.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note, <see cref="Application.Init(IConsoleDriver, string)"/> will override this configuration setting if called
|
||||
/// with either `driver` or `driverName` specified.
|
||||
/// </remarks>
|
||||
/// <inheritdoc cref="IApplication.ForceDriver"/>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static string ForceDriver
|
||||
{
|
||||
@@ -40,9 +29,34 @@ public static partial class Application // Driver abstractions
|
||||
set => ApplicationImpl.Instance.ForceDriver = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection of sixel images to write out to screen when updating.
|
||||
/// Only add to this collection if you are sure terminal supports sixel format.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IApplication.Sixel"/>
|
||||
public static List<SixelToRender> Sixel => ApplicationImpl.Instance.Sixel;
|
||||
}
|
||||
|
||||
/// <summary>Gets a list of <see cref="IDriver"/> types and type names that are available.</summary>
|
||||
/// <returns></returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
public static (List<Type?>, List<string?>) GetDriverTypes ()
|
||||
{
|
||||
// use reflection to get the list of drivers
|
||||
List<Type?> driverTypes = new ();
|
||||
|
||||
// Only inspect the IDriver assembly
|
||||
var asm = typeof (IDriver).Assembly;
|
||||
|
||||
foreach (Type? type in asm.GetTypes ())
|
||||
{
|
||||
if (typeof (IDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
|
||||
{
|
||||
driverTypes.Add (type);
|
||||
}
|
||||
}
|
||||
|
||||
List<string?> driverTypeNames = driverTypes
|
||||
.Where (d => !typeof (IDriver).IsAssignableFrom (d))
|
||||
.Select (d => d!.Name)
|
||||
.Union (["dotnet", "windows", "unix", "fake"])
|
||||
.ToList ()!;
|
||||
|
||||
return (driverTypes, driverTypeNames);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ namespace Terminal.Gui.App;
|
||||
|
||||
public static partial class Application // Keyboard handling
|
||||
{
|
||||
/// <summary>
|
||||
/// Static reference to the current <see cref="IApplication"/> <see cref="IKeyboard"/>.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IApplication.Keyboard"/>
|
||||
public static IKeyboard Keyboard
|
||||
{
|
||||
get => ApplicationImpl.Instance.Keyboard;
|
||||
@@ -14,40 +12,14 @@ public static partial class Application // Keyboard handling
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
|
||||
/// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
|
||||
/// if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be used to simulate key press events.</remarks>
|
||||
/// <param name="key"></param>
|
||||
/// <returns><see langword="true"/> if the key was handled.</returns>
|
||||
public static bool RaiseKeyDownEvent (Key key) => Keyboard.RaiseKeyDownEvent (key);
|
||||
/// <inheritdoc cref="IKeyboard.RaiseKeyDownEvent"/>
|
||||
public static bool RaiseKeyDownEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyDownEvent (key);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes any commands bound at the Application-level to <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns>
|
||||
/// <see langword="null"/> if no command was found; input processing should continue.
|
||||
/// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
|
||||
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
|
||||
/// </returns>
|
||||
public static bool? InvokeCommandsBoundToKey (Key key) => Keyboard.InvokeCommandsBoundToKey (key);
|
||||
/// <inheritdoc cref="IKeyboard.InvokeCommandsBoundToKey"/>
|
||||
public static bool? InvokeCommandsBoundToKey (Key key) => ApplicationImpl.Instance.Keyboard.InvokeCommandsBoundToKey (key);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an Application-bound command.
|
||||
/// </summary>
|
||||
/// <param name="command">The Command to invoke</param>
|
||||
/// <param name="key">The Application-bound Key that was pressed.</param>
|
||||
/// <param name="binding">Describes the binding.</param>
|
||||
/// <returns>
|
||||
/// <see langword="null"/> if no command was found; input processing should continue.
|
||||
/// <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
|
||||
/// <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
|
||||
/// </returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => Keyboard.InvokeCommand (command, key, binding);
|
||||
/// <inheritdoc cref="IKeyboard.InvokeCommand"/>
|
||||
public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => ApplicationImpl.Instance.Keyboard.InvokeCommand (command, key, binding);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user presses a key.
|
||||
@@ -63,29 +35,13 @@ public static partial class Application // Keyboard handling
|
||||
/// </remarks>
|
||||
public static event EventHandler<Key>? KeyDown
|
||||
{
|
||||
add => Keyboard.KeyDown += value;
|
||||
remove => Keyboard.KeyDown -= value;
|
||||
add => ApplicationImpl.Instance.Keyboard.KeyDown += value;
|
||||
remove => ApplicationImpl.Instance.Keyboard.KeyDown -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
|
||||
/// <see cref="KeyUp"/>
|
||||
/// event
|
||||
/// then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be used to simulate key release events.</remarks>
|
||||
/// <param name="key"></param>
|
||||
/// <returns><see langword="true"/> if the key was handled.</returns>
|
||||
public static bool RaiseKeyUpEvent (Key key) => Keyboard.RaiseKeyUpEvent (key);
|
||||
/// <inheritdoc cref="IKeyboard.RaiseKeyUpEvent"/>
|
||||
public static bool RaiseKeyUpEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyUpEvent (key);
|
||||
|
||||
/// <summary>Gets the Application-scoped key bindings.</summary>
|
||||
public static KeyBindings KeyBindings => Keyboard.KeyBindings;
|
||||
|
||||
internal static void AddKeyBindings ()
|
||||
{
|
||||
if (Keyboard is KeyboardImpl keyboard)
|
||||
{
|
||||
keyboard.AddKeyBindings ();
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="IKeyboard.KeyBindings"/>
|
||||
public static KeyBindings KeyBindings => ApplicationImpl.Instance.Keyboard.KeyBindings;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Microsoft.VisualBasic;
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Drivers;
|
||||
using Terminal.Gui.Views;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
@@ -11,7 +15,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
/// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
|
||||
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
|
||||
/// <para>
|
||||
/// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
|
||||
/// This function loads the right <see cref="IDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
|
||||
/// assigns it to <see cref="Top"/>
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -22,90 +26,36 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="Run{T}"/> function combines
|
||||
/// <see cref="Init(IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
|
||||
/// <see cref="Init(IDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
|
||||
/// into a single
|
||||
/// call. An application can use <see cref="Run{T}"/> without explicitly calling
|
||||
/// <see cref="Init(IConsoleDriver,string)"/>.
|
||||
/// <see cref="Init(IDriver,string)"/>.
|
||||
/// </para>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or
|
||||
/// The <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or
|
||||
/// <paramref name="driverName"/> are specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
/// <param name="driverName">
|
||||
/// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the
|
||||
/// <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
|
||||
/// <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
|
||||
/// specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public static void Init (IConsoleDriver? driver = null, string? driverName = null)
|
||||
public static void Init (IDriver? driver = null, string? driverName = null)
|
||||
{
|
||||
ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
|
||||
}
|
||||
|
||||
internal static int MainThreadId
|
||||
/// <summary>
|
||||
/// Gets or sets the main thread ID for the application.
|
||||
/// </summary>
|
||||
public static int? MainThreadId
|
||||
{
|
||||
get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId;
|
||||
set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
|
||||
}
|
||||
|
||||
internal static void SubscribeDriverEvents ()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (Driver);
|
||||
|
||||
Driver.SizeChanged += Driver_SizeChanged;
|
||||
Driver.KeyDown += Driver_KeyDown;
|
||||
Driver.KeyUp += Driver_KeyUp;
|
||||
Driver.MouseEvent += Driver_MouseEvent;
|
||||
}
|
||||
|
||||
internal static void UnsubscribeDriverEvents ()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (Driver);
|
||||
|
||||
Driver.SizeChanged -= Driver_SizeChanged;
|
||||
Driver.KeyDown -= Driver_KeyDown;
|
||||
Driver.KeyUp -= Driver_KeyUp;
|
||||
Driver.MouseEvent -= Driver_MouseEvent;
|
||||
}
|
||||
|
||||
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
RaiseScreenChangedEvent (new Rectangle (new (0, 0), e.Size!.Value));
|
||||
}
|
||||
private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
|
||||
private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
|
||||
private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
|
||||
|
||||
/// <summary>Gets a list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
|
||||
/// <returns></returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
public static (List<Type?>, List<string?>) GetDriverTypes ()
|
||||
{
|
||||
// use reflection to get the list of drivers
|
||||
List<Type?> driverTypes = new ();
|
||||
|
||||
// Only inspect the IConsoleDriver assembly
|
||||
var asm = typeof (IConsoleDriver).Assembly;
|
||||
|
||||
foreach (Type? type in asm.GetTypes ())
|
||||
{
|
||||
if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
|
||||
type is { IsAbstract: false, IsClass: true })
|
||||
{
|
||||
driverTypes.Add (type);
|
||||
}
|
||||
}
|
||||
|
||||
List<string?> driverTypeNames = driverTypes
|
||||
.Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
|
||||
.Select (d => d!.Name)
|
||||
.Union (["dotnet", "windows", "unix", "fake"])
|
||||
.ToList ()!;
|
||||
|
||||
return (driverTypes, driverTypeNames);
|
||||
}
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Shutdown must be called for every call to <see cref="Init"/> or
|
||||
@@ -129,19 +79,17 @@ public static partial class Application // Lifecycle (Init/Shutdown)
|
||||
internal set => ApplicationImpl.Instance.Initialized = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intended to support unit tests that need to know when the application has been initialized.
|
||||
/// </remarks>
|
||||
public static event EventHandler<EventArgs<bool>>? InitializedChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="InitializedChanged"/> event.
|
||||
/// </summary>
|
||||
internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
|
||||
/// <inheritdoc cref="IApplication.InitializedChanged"/>
|
||||
public static event EventHandler<EventArgs<bool>>? InitializedChanged
|
||||
{
|
||||
Application.InitializedChanged?.Invoke (sender, e);
|
||||
add => ApplicationImpl.Instance.InitializedChanged += value;
|
||||
remove => ApplicationImpl.Instance.InitializedChanged -= value;
|
||||
}
|
||||
|
||||
// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
|
||||
// Encapsulate all setting of initial state for Application; Having
|
||||
// this in a function like this ensures we don't make mistakes in
|
||||
// guaranteeing that the state of this singleton is deterministic when Init
|
||||
// starts running and after Shutdown returns.
|
||||
internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed);
|
||||
}
|
||||
|
||||
@@ -92,10 +92,8 @@ public static partial class Application // Mouse handling
|
||||
/// </summary>
|
||||
/// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
|
||||
/// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
|
||||
internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { Mouse.RaiseMouseEvent (mouseEvent); }
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Clears mouse state during application reset.
|
||||
/// </summary>
|
||||
internal static void ResetMouseState () { Mouse.ResetState (); }
|
||||
internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
|
||||
{
|
||||
Mouse.RaiseMouseEvent (mouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@ public static partial class Application // Navigation stuff
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key NextTabGroupKey
|
||||
{
|
||||
get => Keyboard.NextTabGroupKey;
|
||||
set => Keyboard.NextTabGroupKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.NextTabGroupKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.NextTabGroupKey = value;
|
||||
}
|
||||
|
||||
/// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key NextTabKey
|
||||
{
|
||||
get => Keyboard.NextTabKey;
|
||||
set => Keyboard.NextTabKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.NextTabKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.NextTabKey = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,23 +43,23 @@ public static partial class Application // Navigation stuff
|
||||
/// </remarks>
|
||||
public static event EventHandler<Key>? KeyUp
|
||||
{
|
||||
add => Keyboard.KeyUp += value;
|
||||
remove => Keyboard.KeyUp -= value;
|
||||
add => ApplicationImpl.Instance.Keyboard.KeyUp += value;
|
||||
remove => ApplicationImpl.Instance.Keyboard.KeyUp -= value;
|
||||
}
|
||||
|
||||
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key PrevTabGroupKey
|
||||
{
|
||||
get => Keyboard.PrevTabGroupKey;
|
||||
set => Keyboard.PrevTabGroupKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey = value;
|
||||
}
|
||||
|
||||
/// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key PrevTabKey
|
||||
{
|
||||
get => Keyboard.PrevTabKey;
|
||||
set => Keyboard.PrevTabKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.PrevTabKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.PrevTabKey = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
@@ -10,498 +9,87 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key QuitKey
|
||||
{
|
||||
get => Keyboard.QuitKey;
|
||||
set => Keyboard.QuitKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.QuitKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.QuitKey = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
|
||||
[ConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static Key ArrangeKey
|
||||
{
|
||||
get => Keyboard.ArrangeKey;
|
||||
set => Keyboard.ArrangeKey = value;
|
||||
get => ApplicationImpl.Instance.Keyboard.ArrangeKey;
|
||||
set => ApplicationImpl.Instance.Keyboard.ArrangeKey = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is
|
||||
/// created in <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
|
||||
/// must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
|
||||
/// when the application is done.
|
||||
/// </remarks>
|
||||
public static event EventHandler<RunStateEventArgs>? NotifyNewRunState;
|
||||
/// <inheritdoc cref="IApplication.Begin"/>
|
||||
public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel);
|
||||
|
||||
/// <summary>Notify that an existent <see cref="RunState"/> is stopping (<see cref="End(RunState)"/> was called).</summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
|
||||
/// must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
|
||||
/// when the application is done.
|
||||
/// </remarks>
|
||||
#pragma warning disable CS0067 // Event is never used
|
||||
#pragma warning disable CS0414 // Event is never used
|
||||
public static event EventHandler<ToplevelEventArgs>? NotifyStopRunState;
|
||||
#pragma warning restore CS0414 // Event is never used
|
||||
#pragma warning restore CS0067 // Event is never used
|
||||
/// <inheritdoc cref="IApplication.PositionCursor"/>
|
||||
public static bool PositionCursor () => ApplicationImpl.Instance.PositionCursor ();
|
||||
|
||||
/// <summary>Building block API: Prepares the provided <see cref="Toplevel"/> for execution.</summary>
|
||||
/// <returns>
|
||||
/// The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon
|
||||
/// completion.
|
||||
/// </returns>
|
||||
/// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
|
||||
/// <remarks>
|
||||
/// This method prepares the provided <see cref="Toplevel"/> for running with the focus, it adds this to the list
|
||||
/// of <see cref="Toplevel"/>s, lays out the SubViews, focuses the first element, and draws the <see cref="Toplevel"/>
|
||||
/// in the screen. This is usually followed by executing the <see cref="RunLoop"/> method, and then the
|
||||
/// <see cref="End(RunState)"/> method upon termination which will undo these changes.
|
||||
/// </remarks>
|
||||
public static RunState Begin (Toplevel toplevel)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (toplevel);
|
||||
|
||||
// Ensure the mouse is ungrabbed.
|
||||
if (Mouse.MouseGrabView is { })
|
||||
{
|
||||
Mouse.UngrabMouse ();
|
||||
}
|
||||
|
||||
var rs = new RunState (toplevel);
|
||||
|
||||
#if DEBUG_IDISPOSABLE
|
||||
if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
|
||||
{
|
||||
// This assertion confirm if the Top was already disposed
|
||||
Debug.Assert (Top.WasDisposed);
|
||||
Debug.Assert (Top == CachedRunStateToplevel);
|
||||
}
|
||||
#endif
|
||||
|
||||
lock (TopLevels)
|
||||
{
|
||||
if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
|
||||
{
|
||||
// If Top was already disposed and isn't on the Toplevels Stack,
|
||||
// clean it up here if is the same as _cachedRunStateToplevel
|
||||
if (Top == CachedRunStateToplevel)
|
||||
{
|
||||
Top = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Probably this will never hit
|
||||
throw new ObjectDisposedException (Top.GetType ().FullName);
|
||||
}
|
||||
}
|
||||
|
||||
// BUGBUG: We should not depend on `Id` internally.
|
||||
// BUGBUG: It is super unclear what this code does anyway.
|
||||
if (string.IsNullOrEmpty (toplevel.Id))
|
||||
{
|
||||
var count = 1;
|
||||
var id = (TopLevels.Count + count).ToString ();
|
||||
|
||||
while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { })
|
||||
{
|
||||
count++;
|
||||
id = (TopLevels.Count + count).ToString ();
|
||||
}
|
||||
|
||||
toplevel.Id = (TopLevels.Count + count).ToString ();
|
||||
|
||||
TopLevels.Push (toplevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id);
|
||||
|
||||
if (dup is null)
|
||||
{
|
||||
TopLevels.Push (toplevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Top is null)
|
||||
{
|
||||
Top = toplevel;
|
||||
}
|
||||
|
||||
if ((Top?.Modal == false && toplevel.Modal)
|
||||
|| (Top?.Modal == false && !toplevel.Modal)
|
||||
|| (Top?.Modal == true && toplevel.Modal))
|
||||
{
|
||||
if (toplevel.Visible)
|
||||
{
|
||||
if (Top is { HasFocus: true })
|
||||
{
|
||||
Top.HasFocus = false;
|
||||
}
|
||||
|
||||
// Force leave events for any entered views in the old Top
|
||||
if (GetLastMousePosition () is { })
|
||||
{
|
||||
RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new ());
|
||||
}
|
||||
|
||||
Top?.OnDeactivate (toplevel);
|
||||
Toplevel previousTop = Top!;
|
||||
|
||||
Top = toplevel;
|
||||
Top.OnActivate (previousTop);
|
||||
}
|
||||
}
|
||||
|
||||
// View implements ISupportInitializeNotification which is derived from ISupportInitialize
|
||||
if (!toplevel.IsInitialized)
|
||||
{
|
||||
toplevel.BeginInit ();
|
||||
toplevel.EndInit (); // Calls Layout
|
||||
}
|
||||
|
||||
// Try to set initial focus to any TabStop
|
||||
if (!toplevel.HasFocus)
|
||||
{
|
||||
toplevel.SetFocus ();
|
||||
}
|
||||
|
||||
toplevel.OnLoaded ();
|
||||
|
||||
ApplicationImpl.Instance.LayoutAndDraw (true);
|
||||
|
||||
if (PositionCursor ())
|
||||
{
|
||||
Driver?.UpdateCursor ();
|
||||
}
|
||||
|
||||
NotifyNewRunState?.Invoke (toplevel, new (rs));
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="View.PositionCursor"/> on the most focused view.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does nothing if there is no most focused view.
|
||||
/// <para>
|
||||
/// If the most focused view is not visible within it's superview, the cursor will be hidden.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
|
||||
internal static bool PositionCursor ()
|
||||
{
|
||||
// Find the most focused view and position the cursor there.
|
||||
View? mostFocused = Navigation?.GetFocused ();
|
||||
|
||||
// If the view is not visible or enabled, don't position the cursor
|
||||
if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
|
||||
{
|
||||
var current = CursorVisibility.Invisible;
|
||||
Driver?.GetCursorVisibility (out current);
|
||||
|
||||
if (current != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver?.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the view is not visible within it's superview, don't position the cursor
|
||||
Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
|
||||
|
||||
Rectangle superViewViewport =
|
||||
mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
|
||||
|
||||
if (!superViewViewport.IntersectsWith (mostFocusedViewport))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Point? cursor = mostFocused.PositionCursor ();
|
||||
|
||||
Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility);
|
||||
|
||||
if (cursor is { })
|
||||
{
|
||||
// Convert cursor to screen coords
|
||||
cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location;
|
||||
|
||||
// If the cursor is not in a visible location in the SuperView, hide it
|
||||
if (!superViewViewport.Contains (cursor.Value))
|
||||
{
|
||||
if (currentCursorVisibility != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show it
|
||||
if (currentCursorVisibility == CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (mostFocused.CursorVisibility);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentCursorVisibility != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/> object and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
|
||||
/// <inheritdoc cref="IApplication.Run(Func{Exception, bool}, string)"/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
{
|
||||
return ApplicationImpl.Instance.Run (errorHandler, driver);
|
||||
}
|
||||
public static Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null) => ApplicationImpl.Instance.Run (errorHandler, driver);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="errorHandler"></param>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
|
||||
/// be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
|
||||
/// </param>
|
||||
/// <returns>The created T object. The caller is responsible for disposing this object.</returns>
|
||||
/// <inheritdoc cref="IApplication.Run{TView}(Func{Exception, bool}, string)"/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
where T : Toplevel, new()
|
||||
public static TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
|
||||
where TView : Toplevel, new() => ApplicationImpl.Instance.Run<TView> (errorHandler, driver);
|
||||
|
||||
/// <inheritdoc cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>
|
||||
public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) => ApplicationImpl.Instance.Run (view, errorHandler);
|
||||
|
||||
/// <inheritdoc cref="IApplication.AddTimeout"/>
|
||||
public static object? AddTimeout (TimeSpan time, Func<bool> callback) => ApplicationImpl.Instance.AddTimeout (time, callback);
|
||||
|
||||
/// <inheritdoc cref="IApplication.RemoveTimeout"/>
|
||||
public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token);
|
||||
|
||||
/// <inheritdoc cref="IApplication.TimedEvents"/>
|
||||
///
|
||||
public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents;
|
||||
/// <inheritdoc cref="IApplication.Invoke"/>
|
||||
public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action);
|
||||
|
||||
/// <inheritdoc cref="IApplication.LayoutAndDraw"/>
|
||||
public static void LayoutAndDraw (bool forceRedraw = false) => ApplicationImpl.Instance.LayoutAndDraw (forceRedraw);
|
||||
|
||||
/// <inheritdoc cref="IApplication.StopAfterFirstIteration"/>
|
||||
public static bool StopAfterFirstIteration
|
||||
{
|
||||
return ApplicationImpl.Instance.Run<T> (errorHandler, driver);
|
||||
get => ApplicationImpl.Instance.StopAfterFirstIteration;
|
||||
set => ApplicationImpl.Instance.StopAfterFirstIteration = value;
|
||||
}
|
||||
|
||||
/// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is used to start processing events for the main application, but it is also used to run other
|
||||
/// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To make a <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
|
||||
/// <see cref="Application.RequestStop"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
|
||||
/// <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling
|
||||
/// <see cref="End(RunState)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Alternatively, to have a program control the main loop and process events manually, call
|
||||
/// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call
|
||||
/// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
|
||||
/// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
|
||||
/// return control immediately.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When using <see cref="Run{T}"/> or
|
||||
/// <see cref="Run(System.Func{System.Exception,bool},IConsoleDriver)"/>
|
||||
/// <see cref="Init"/> will be called automatically.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
|
||||
/// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
|
||||
/// returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will
|
||||
/// exit.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
|
||||
/// <param name="errorHandler">
|
||||
/// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
|
||||
/// rethrows when null).
|
||||
/// </param>
|
||||
public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { ApplicationImpl.Instance.Run (view, errorHandler); }
|
||||
/// <inheritdoc cref="IApplication.RequestStop(Toplevel)"/>
|
||||
public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top);
|
||||
|
||||
/// <summary>Adds a timeout to the application.</summary>
|
||||
/// <remarks>
|
||||
/// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
|
||||
/// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
|
||||
/// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
|
||||
/// </remarks>
|
||||
public static object? AddTimeout (TimeSpan time, Func<bool> callback) { return ApplicationImpl.Instance.AddTimeout (time, callback); }
|
||||
/// <inheritdoc cref="IApplication.End"/>
|
||||
public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken);
|
||||
|
||||
/// <summary>Removes a previously scheduled timeout</summary>
|
||||
/// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
|
||||
/// Returns
|
||||
/// <see langword="true"/>
|
||||
/// if the timeout is successfully removed; otherwise,
|
||||
/// <see langword="false"/>
|
||||
/// .
|
||||
/// This method also returns
|
||||
/// <see langword="false"/>
|
||||
/// if the timeout is not found.
|
||||
public static bool RemoveTimeout (object token) { return ApplicationImpl.Instance.RemoveTimeout (token); }
|
||||
/// <inheritdoc cref="IApplication.RaiseIteration"/>
|
||||
internal static void RaiseIteration () => ApplicationImpl.Instance.RaiseIteration ();
|
||||
|
||||
/// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary>
|
||||
/// <param name="action">the action to be invoked on the main processing thread.</param>
|
||||
public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); }
|
||||
|
||||
/// <summary>
|
||||
/// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
|
||||
/// need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
|
||||
/// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
|
||||
/// </summary>
|
||||
/// <param name="forceRedraw">
|
||||
/// If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
|
||||
/// should only be overriden for testing.
|
||||
/// </param>
|
||||
public static void LayoutAndDraw (bool forceRedraw = false)
|
||||
/// <inheritdoc cref="IApplication.Iteration"/>
|
||||
public static event EventHandler<IterationEventArgs>? Iteration
|
||||
{
|
||||
ApplicationImpl.Instance.LayoutAndDraw (forceRedraw);
|
||||
add => ApplicationImpl.Instance.Iteration += value;
|
||||
remove => ApplicationImpl.Instance.Iteration -= value;
|
||||
}
|
||||
|
||||
/// <summary>This event is raised on each iteration of the main loop.</summary>
|
||||
/// <remarks>See also <see cref="Timeout"/></remarks>
|
||||
public static event EventHandler<IterationEventArgs>? Iteration;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
|
||||
/// cause the application to continue running until Application.RequestStop () is called.
|
||||
/// </summary>
|
||||
public static bool EndAfterFirstIteration { get; set; }
|
||||
|
||||
/// <summary>Building block API: Runs the main loop for the created <see cref="Toplevel"/>.</summary>
|
||||
/// <param name="state">The state returned by the <see cref="Begin(Toplevel)"/> method.</param>
|
||||
public static void RunLoop (RunState state)
|
||||
/// <inheritdoc cref="IApplication.SessionBegun"/>
|
||||
public static event EventHandler<SessionTokenEventArgs>? 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;
|
||||
}
|
||||
|
||||
/// <summary>Run one application iteration.</summary>
|
||||
/// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
|
||||
/// <param name="firstIteration">
|
||||
/// Set to <see langword="true"/> if this is the first run loop iteration.
|
||||
/// </param>
|
||||
/// <returns><see langword="false"/> if at least one iteration happened.</returns>
|
||||
public static bool RunIteration (ref RunState state, bool firstIteration = false)
|
||||
/// <inheritdoc cref="IApplication.SessionEnded"/>
|
||||
public static event EventHandler<ToplevelEventArgs>? SessionEnded
|
||||
{
|
||||
ApplicationImpl appImpl = (ApplicationImpl)ApplicationImpl.Instance;
|
||||
appImpl.Coordinator?.RunIteration ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
|
||||
/// <param name="top">The <see cref="Toplevel"/> to stop.</param>
|
||||
/// <remarks>
|
||||
/// <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
|
||||
/// <para>
|
||||
/// Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the
|
||||
/// <see cref="Toplevel.Running"/>
|
||||
/// property on the currently running <see cref="Toplevel"/> to false.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void RequestStop (Toplevel? top = null) { ApplicationImpl.Instance.RequestStop (top); }
|
||||
|
||||
/// <summary>
|
||||
/// Building block API: completes the execution of a <see cref="Toplevel"/> that was started with
|
||||
/// <see cref="Begin(Toplevel)"/> .
|
||||
/// </summary>
|
||||
/// <param name="runState">The <see cref="RunState"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
|
||||
public static void End (RunState runState)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,23 @@ namespace Terminal.Gui.App;
|
||||
|
||||
public static partial class Application // Screen related stuff; intended to hide Driver details
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If the <see cref="IConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc cref="IApplication.Screen"/>
|
||||
|
||||
public static Rectangle Screen
|
||||
{
|
||||
get => ApplicationImpl.Instance.Screen;
|
||||
set => ApplicationImpl.Instance.Screen = value;
|
||||
}
|
||||
|
||||
/// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
|
||||
public static event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the application's size has changed. Sets the size of all <see cref="Toplevel"/>s and fires the
|
||||
/// <see cref="ScreenChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="screen">The new screen size and position.</param>
|
||||
public static void RaiseScreenChangedEvent (Rectangle screen)
|
||||
/// <inheritdoc cref="IApplication.ScreenChanged"/>
|
||||
public static event EventHandler<EventArgs<Rectangle>>? ScreenChanged
|
||||
{
|
||||
Screen = new (Point.Empty, screen.Size);
|
||||
|
||||
ScreenChanged?.Invoke (ApplicationImpl.Instance, new (screen));
|
||||
|
||||
foreach (Toplevel t in TopLevels)
|
||||
{
|
||||
t.OnSizeChanging (new (screen.Size));
|
||||
t.SetNeedsLayout ();
|
||||
}
|
||||
|
||||
LayoutAndDraw (true);
|
||||
add => ApplicationImpl.Instance.ScreenChanged += value;
|
||||
remove => ApplicationImpl.Instance.ScreenChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is typical set to true when a View's <see cref="View.Frame"/> changes and that view has no
|
||||
/// SuperView (e.g. when <see cref="Application.Top"/> is moved or resized.
|
||||
/// </remarks>
|
||||
/// <inheritdoc cref="IApplication.ClearScreenNextIteration"/>
|
||||
|
||||
internal static bool ClearScreenNextIteration
|
||||
{
|
||||
get => ApplicationImpl.Instance.ClearScreenNextIteration;
|
||||
|
||||
@@ -5,10 +5,8 @@ namespace Terminal.Gui.App;
|
||||
|
||||
public static partial class Application // Toplevel handling
|
||||
{
|
||||
// BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
|
||||
|
||||
/// <summary>Holds the stack of TopLevel views.</summary>
|
||||
internal static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
|
||||
/// <inheritdoc cref="IApplication.TopLevels"/>
|
||||
public static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
|
||||
|
||||
/// <summary>The <see cref="Toplevel"/> that is currently active.</summary>
|
||||
/// <value>The top.</value>
|
||||
@@ -17,12 +15,4 @@ public static partial class Application // Toplevel handling
|
||||
get => ApplicationImpl.Instance.Top;
|
||||
internal set => ApplicationImpl.Instance.Top = value;
|
||||
}
|
||||
|
||||
internal static Toplevel? CachedRunStateToplevel
|
||||
{
|
||||
get => ApplicationImpl.Instance.CachedRunStateToplevel;
|
||||
private set => ApplicationImpl.Instance.CachedRunStateToplevel = value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -36,19 +36,19 @@
|
||||
<FileName>App\MainLoopSyncContext.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.App.RunState" Collapsed="true" BaseTypeListCollapsed="true">
|
||||
<Class Name="Terminal.Gui.App.SessionToken" Collapsed="true" BaseTypeListCollapsed="true">
|
||||
<Position X="15.25" Y="4" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA=</HashCode>
|
||||
<FileName>App\RunState.cs</FileName>
|
||||
<FileName>App\SessionToken.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" Collapsed="true" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.App.RunStateEventArgs" Collapsed="true">
|
||||
<Class Name="Terminal.Gui.App.SessionTokenEventArgs" Collapsed="true">
|
||||
<Position X="17" Y="4" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=</HashCode>
|
||||
<FileName>App\RunStateEventArgs.cs</FileName>
|
||||
<FileName>App\SessionTokenEventArgs.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.App.Timeout" Collapsed="true">
|
||||
|
||||
@@ -39,18 +39,6 @@ namespace Terminal.Gui.App;
|
||||
/// <remarks></remarks>
|
||||
public static partial class Application
|
||||
{
|
||||
/// <summary>Gets all cultures supported by the application without the invariant language.</summary>
|
||||
public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures ();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Handles recurring events. These are invoked on the main UI thread - allowing for
|
||||
/// safe updates to <see cref="View"/> instances.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of iterations of the main loop (and hence draws)
|
||||
/// to allow to occur per second. Defaults to <see cref="DefaultMaximumIterationsPerSecond"/>> which is a 40ms sleep
|
||||
@@ -71,7 +59,7 @@ public static partial class Application
|
||||
/// <returns>A string representation of the Application </returns>
|
||||
public new static string ToString ()
|
||||
{
|
||||
IConsoleDriver? driver = Driver;
|
||||
IDriver? driver = Driver;
|
||||
|
||||
if (driver is null)
|
||||
{
|
||||
@@ -82,11 +70,11 @@ public static partial class Application
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string representation of the Application rendered by the provided <see cref="IConsoleDriver"/>.
|
||||
/// Gets a string representation of the Application rendered by the provided <see cref="IDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="driver">The driver to use to render the contents.</param>
|
||||
/// <returns>A string representation of the Application </returns>
|
||||
public static string ToString (IConsoleDriver? driver)
|
||||
public static string ToString (IDriver? driver)
|
||||
{
|
||||
if (driver is null)
|
||||
{
|
||||
@@ -129,6 +117,10 @@ public static partial class Application
|
||||
return sb.ToString ();
|
||||
}
|
||||
|
||||
/// <summary>Gets all cultures supported by the application without the invariant language.</summary>
|
||||
public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures ();
|
||||
|
||||
|
||||
internal static List<CultureInfo> GetAvailableCulturesFromEmbeddedResources ()
|
||||
{
|
||||
ResourceManager rm = new (typeof (Strings));
|
||||
@@ -171,100 +163,4 @@ public static partial class Application
|
||||
// It's called from a self-contained single-file and get available cultures from the embedded resources strings.
|
||||
return GetAvailableCulturesFromEmbeddedResources ();
|
||||
}
|
||||
|
||||
// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
|
||||
// Encapsulate all setting of initial state for Application; Having
|
||||
// this in a function like this ensures we don't make mistakes in
|
||||
// guaranteeing that the state of this singleton is deterministic when Init
|
||||
// starts running and after Shutdown returns.
|
||||
internal static void ResetState (bool ignoreDisposed = false)
|
||||
{
|
||||
// Shutdown is the bookend for Init. As such it needs to clean up all resources
|
||||
// Init created. Apps that do any threading will need to code defensively for this.
|
||||
// e.g. see Issue #537
|
||||
foreach (Toplevel? t in TopLevels)
|
||||
{
|
||||
t!.Running = false;
|
||||
}
|
||||
|
||||
if (Popover?.GetActivePopover () is View popover)
|
||||
{
|
||||
// This forcefully closes the popover; invoking Command.Quit would be more graceful
|
||||
// but since this is shutdown, doing this is ok.
|
||||
popover.Visible = false;
|
||||
}
|
||||
|
||||
Popover?.Dispose ();
|
||||
Popover = null;
|
||||
|
||||
TopLevels.Clear ();
|
||||
#if DEBUG_IDISPOSABLE
|
||||
|
||||
// Don't dispose the Top. It's up to caller dispose it
|
||||
if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
|
||||
{
|
||||
Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
|
||||
|
||||
// If End wasn't called _cachedRunStateToplevel may be null
|
||||
if (CachedRunStateToplevel is { })
|
||||
{
|
||||
Debug.Assert (CachedRunStateToplevel.WasDisposed);
|
||||
Debug.Assert (CachedRunStateToplevel == Top);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Top = null;
|
||||
CachedRunStateToplevel = null;
|
||||
|
||||
MainThreadId = -1;
|
||||
Iteration = null;
|
||||
EndAfterFirstIteration = false;
|
||||
ClearScreenNextIteration = false;
|
||||
|
||||
// Driver stuff
|
||||
if (Driver is { })
|
||||
{
|
||||
UnsubscribeDriverEvents ();
|
||||
Driver?.End ();
|
||||
Driver = null;
|
||||
}
|
||||
|
||||
// Reset Screen to null so it will be recalculated on next access
|
||||
// Note: ApplicationImpl.Shutdown() also calls ResetScreen() before calling this method
|
||||
// to avoid potential circular reference issues. Calling it twice is harmless.
|
||||
if (ApplicationImpl.Instance is ApplicationImpl impl)
|
||||
{
|
||||
impl.ResetScreen ();
|
||||
}
|
||||
|
||||
// Don't reset ForceDriver; it needs to be set before Init is called.
|
||||
//ForceDriver = string.Empty;
|
||||
//Force16Colors = false;
|
||||
_forceFakeConsole = false;
|
||||
|
||||
// Run State stuff
|
||||
NotifyNewRunState = null;
|
||||
NotifyStopRunState = null;
|
||||
// Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access
|
||||
Initialized = false;
|
||||
|
||||
// Mouse
|
||||
// Do not clear _lastMousePosition; Popovers require it to stay set with
|
||||
// last mouse pos.
|
||||
//_lastMousePosition = null;
|
||||
CachedViewsUnderMouse.Clear ();
|
||||
ResetMouseState ();
|
||||
|
||||
// Keyboard events and bindings are now managed by the Keyboard instance
|
||||
|
||||
ScreenChanged = null;
|
||||
|
||||
Navigation = null;
|
||||
|
||||
// Reset synchronization context to allow the user to run async/await,
|
||||
// as the main loop has been ended, the synchronization context from
|
||||
// gui.cs does no longer process any callbacks. See #1084 for more details:
|
||||
// (https://github.com/gui-cs/Terminal.Gui/issues/1084).
|
||||
SynchronizationContext.SetSynchronizationContext (null);
|
||||
}
|
||||
}
|
||||
|
||||
176
Terminal.Gui/App/ApplicationImpl.Driver.cs
Normal file
176
Terminal.Gui/App/ApplicationImpl.Driver.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public partial class ApplicationImpl
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IDriver? Driver { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Force16Colors { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ForceDriver { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<SixelToRender> Sixel { get; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the appropriate <see cref="IDriver"/> based on platform and driverName.
|
||||
/// </summary>
|
||||
/// <param name="driverName"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private void CreateDriver (string? driverName)
|
||||
{
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
// Check component factory type first - this takes precedence over driverName
|
||||
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
|
||||
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
|
||||
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
|
||||
// Then check driverName
|
||||
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
|
||||
// Decide which driver to use - component factory type takes priority
|
||||
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
|
||||
_driverName = "fake";
|
||||
}
|
||||
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
_driverName = "windows";
|
||||
}
|
||||
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
|
||||
_driverName = "dotnet";
|
||||
}
|
||||
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
_driverName = "unix";
|
||||
}
|
||||
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
_driverName = "windows";
|
||||
}
|
||||
else if (p == PlatformID.Unix)
|
||||
{
|
||||
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
_driverName = "unix";
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Information($"Falling back to dotnet driver.");
|
||||
Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
|
||||
_driverName = "dotnet";
|
||||
}
|
||||
|
||||
Logging.Trace ($"Created Subcomponents: {Coordinator}");
|
||||
|
||||
Coordinator.StartInputTaskAsync ().Wait ();
|
||||
|
||||
if (Driver == null)
|
||||
{
|
||||
throw new ("Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IComponentFactory? _componentFactory;
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Gets or sets the main loop coordinator that orchestrates the application's event processing,
|
||||
/// input handling, and rendering pipeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="IMainLoopCoordinator"/> is the central component responsible for:
|
||||
/// <list type="bullet">
|
||||
/// <item>Managing the platform-specific input thread that reads from the console</item>
|
||||
/// <item>Coordinating the main application loop via <see cref="IMainLoopCoordinator.RunIteration"/></item>
|
||||
/// <item>Processing queued input events and translating them to Terminal.Gui events</item>
|
||||
/// <item>Managing the <see cref="ApplicationMainLoop{TInputRecord}"/> that handles rendering</item>
|
||||
/// <item>Executing scheduled timeouts and callbacks via <see cref="ITimedEvents"/></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The coordinator is created in <see cref="CreateDriver"/> based on the selected driver
|
||||
/// (Windows, Unix, .NET, or Fake) and is started by calling
|
||||
/// <see cref="IMainLoopCoordinator.StartInputTaskAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal IMainLoopCoordinator? Coordinator { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Creates a <see cref="MainLoopCoordinator{TInputRecord}"/> with the appropriate component factory
|
||||
/// for the specified input record type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInputRecord">
|
||||
/// Platform-specific input type: <see cref="ConsoleKeyInfo"/> (.NET/Fake),
|
||||
/// <see cref="WindowsConsole.InputRecord"/> (Windows), or <see cref="char"/> (Unix).
|
||||
/// </typeparam>
|
||||
/// <param name="fallbackFactory">
|
||||
/// Factory function to create the component factory if <see cref="_componentFactory"/>
|
||||
/// is not of type <see cref="IComponentFactory{TInputRecord}"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="MainLoopCoordinator{TInputRecord}"/> configured with the input queue,
|
||||
/// main loop, timed events, and selected component factory.
|
||||
/// </returns>
|
||||
private IMainLoopCoordinator CreateSubcomponents<TInputRecord> (Func<IComponentFactory<TInputRecord>> fallbackFactory) where TInputRecord : struct
|
||||
{
|
||||
ConcurrentQueue<TInputRecord> inputQueue = new ();
|
||||
ApplicationMainLoop<TInputRecord> loop = new ();
|
||||
|
||||
IComponentFactory<TInputRecord> cf;
|
||||
|
||||
if (_componentFactory is IComponentFactory<TInputRecord> typedFactory)
|
||||
{
|
||||
cf = typedFactory;
|
||||
}
|
||||
else
|
||||
{
|
||||
cf = fallbackFactory ();
|
||||
}
|
||||
|
||||
return new MainLoopCoordinator<TInputRecord> (_timedEvents, inputQueue, loop, cf);
|
||||
}
|
||||
|
||||
internal void SubscribeDriverEvents ()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (Driver);
|
||||
|
||||
Driver.SizeChanged += Driver_SizeChanged;
|
||||
Driver.KeyDown += Driver_KeyDown;
|
||||
Driver.KeyUp += Driver_KeyUp;
|
||||
Driver.MouseEvent += Driver_MouseEvent;
|
||||
}
|
||||
|
||||
internal void UnsubscribeDriverEvents ()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (Driver);
|
||||
|
||||
Driver.SizeChanged -= Driver_SizeChanged;
|
||||
Driver.KeyDown -= Driver_KeyDown;
|
||||
Driver.KeyUp -= Driver_KeyUp;
|
||||
Driver.MouseEvent -= Driver_MouseEvent;
|
||||
}
|
||||
|
||||
private void Driver_KeyDown (object? sender, Key e) { Keyboard?.RaiseKeyDownEvent (e); }
|
||||
|
||||
private void Driver_KeyUp (object? sender, Key e) { Keyboard?.RaiseKeyUpEvent (e); }
|
||||
|
||||
private void Driver_MouseEvent (object? sender, MouseEventArgs e) { Mouse?.RaiseMouseEvent (e); }
|
||||
}
|
||||
251
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
Normal file
251
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public partial class ApplicationImpl
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Initialized { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<EventArgs<bool>>? InitializedChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public void Init (IDriver? driver = null, string? driverName = null)
|
||||
{
|
||||
if (Initialized)
|
||||
{
|
||||
Logging.Error ("Init called multiple times without shutdown, aborting.");
|
||||
|
||||
throw new InvalidOperationException ("Init called multiple times without Shutdown");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace (driverName))
|
||||
{
|
||||
_driverName = driverName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace (_driverName))
|
||||
{
|
||||
_driverName = ForceDriver;
|
||||
}
|
||||
|
||||
Debug.Assert (Navigation is null);
|
||||
Navigation = new ();
|
||||
|
||||
Debug.Assert (Popover is null);
|
||||
Popover = new ();
|
||||
|
||||
// Preserve existing keyboard settings if they exist
|
||||
bool hasExistingKeyboard = _keyboard is { };
|
||||
Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
|
||||
Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
|
||||
Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
|
||||
Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
|
||||
Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
|
||||
Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
|
||||
|
||||
// Reset keyboard to ensure fresh state with default bindings
|
||||
_keyboard = new KeyboardImpl { Application = this };
|
||||
|
||||
// Restore previously set keys if they existed and were different from defaults
|
||||
if (hasExistingKeyboard)
|
||||
{
|
||||
_keyboard.QuitKey = existingQuitKey;
|
||||
_keyboard.ArrangeKey = existingArrangeKey;
|
||||
_keyboard.NextTabKey = existingNextTabKey;
|
||||
_keyboard.PrevTabKey = existingPrevTabKey;
|
||||
_keyboard.NextTabGroupKey = existingNextTabGroupKey;
|
||||
_keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
|
||||
}
|
||||
|
||||
CreateDriver (driverName ?? _driverName);
|
||||
Screen = Driver!.Screen;
|
||||
Initialized = true;
|
||||
|
||||
RaiseInitializedChanged (this, new (true));
|
||||
SubscribeDriverEvents ();
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext (new ());
|
||||
MainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
}
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
public void Shutdown ()
|
||||
{
|
||||
// Stop the coordinator if running
|
||||
Coordinator?.Stop ();
|
||||
|
||||
// Capture state before cleanup
|
||||
bool wasInitialized = Initialized;
|
||||
|
||||
#if DEBUG
|
||||
|
||||
// Check that all Application events have no remaining subscribers BEFORE clearing them
|
||||
// Only check if we were actually initialized
|
||||
if (wasInitialized)
|
||||
{
|
||||
AssertNoEventSubscribers (nameof (Iteration), Iteration);
|
||||
AssertNoEventSubscribers (nameof (SessionBegun), SessionBegun);
|
||||
AssertNoEventSubscribers (nameof (SessionEnded), SessionEnded);
|
||||
AssertNoEventSubscribers (nameof (ScreenChanged), ScreenChanged);
|
||||
|
||||
//AssertNoEventSubscribers (nameof (InitializedChanged), InitializedChanged);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Clean up all application state (including sync context)
|
||||
// ResetState handles the case where Initialized is false
|
||||
ResetState ();
|
||||
|
||||
// Configuration manager diagnostics
|
||||
ConfigurationManager.PrintJsonErrors ();
|
||||
|
||||
// Raise the initialized changed event to notify shutdown
|
||||
if (wasInitialized)
|
||||
{
|
||||
bool init = Initialized; // Will be false after ResetState
|
||||
RaiseInitializedChanged (this, new (in init));
|
||||
}
|
||||
|
||||
// Clear the event to prevent memory leaks
|
||||
InitializedChanged = null;
|
||||
|
||||
// Create a new lazy instance for potential future Init
|
||||
_lazyInstance = new (() => new ApplicationImpl ());
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// DEBUG ONLY: Asserts that an event has no remaining subscribers.
|
||||
/// </summary>
|
||||
/// <param name="eventName">The name of the event for diagnostic purposes.</param>
|
||||
/// <param name="eventDelegate">The event delegate to check.</param>
|
||||
private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate)
|
||||
{
|
||||
if (eventDelegate is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Delegate [] subscribers = eventDelegate.GetInvocationList ();
|
||||
|
||||
if (subscribers.Length > 0)
|
||||
{
|
||||
string subscriberInfo = string.Join (
|
||||
", ",
|
||||
subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}"
|
||||
)
|
||||
);
|
||||
|
||||
Debug.Fail (
|
||||
$"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}"
|
||||
);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ResetState (bool ignoreDisposed = false)
|
||||
{
|
||||
// Shutdown is the bookend for Init. As such it needs to clean up all resources
|
||||
// Init created. Apps that do any threading will need to code defensively for this.
|
||||
// e.g. see Issue #537
|
||||
|
||||
// === 1. Stop all running toplevels ===
|
||||
foreach (Toplevel? t in TopLevels)
|
||||
{
|
||||
t!.Running = false;
|
||||
}
|
||||
|
||||
// === 2. Close and dispose popover ===
|
||||
if (Popover?.GetActivePopover () is View popover)
|
||||
{
|
||||
// This forcefully closes the popover; invoking Command.Quit would be more graceful
|
||||
// but since this is shutdown, doing this is ok.
|
||||
popover.Visible = false;
|
||||
}
|
||||
|
||||
Popover?.Dispose ();
|
||||
Popover = null;
|
||||
|
||||
// === 3. Clean up toplevels ===
|
||||
TopLevels.Clear ();
|
||||
|
||||
#if DEBUG_IDISPOSABLE
|
||||
|
||||
// Don't dispose the Top. It's up to caller dispose it
|
||||
if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
|
||||
{
|
||||
Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
|
||||
|
||||
// If End wasn't called _CachedSessionTokenToplevel may be null
|
||||
if (CachedSessionTokenToplevel is { })
|
||||
{
|
||||
Debug.Assert (CachedSessionTokenToplevel.WasDisposed);
|
||||
Debug.Assert (CachedSessionTokenToplevel == Top);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Top = null;
|
||||
CachedSessionTokenToplevel = null;
|
||||
|
||||
// === 4. Clean up driver ===
|
||||
if (Driver is { })
|
||||
{
|
||||
UnsubscribeDriverEvents ();
|
||||
Driver?.End ();
|
||||
Driver = null;
|
||||
}
|
||||
|
||||
// Reset screen
|
||||
ResetScreen ();
|
||||
_screen = null;
|
||||
|
||||
// === 5. Clear run state ===
|
||||
Iteration = null;
|
||||
SessionBegun = null;
|
||||
SessionEnded = null;
|
||||
StopAfterFirstIteration = false;
|
||||
ClearScreenNextIteration = false;
|
||||
|
||||
// === 6. Reset input systems ===
|
||||
// Mouse and Keyboard will be lazy-initialized on next access
|
||||
_mouse = null;
|
||||
_keyboard = null;
|
||||
Mouse.ResetState ();
|
||||
|
||||
// === 7. Clear navigation and screen state ===
|
||||
ScreenChanged = null;
|
||||
Navigation = null;
|
||||
|
||||
// === 8. Reset initialization state ===
|
||||
Initialized = false;
|
||||
MainThreadId = null;
|
||||
|
||||
// === 9. Clear graphics ===
|
||||
Sixel.Clear ();
|
||||
|
||||
// === 10. Reset synchronization context ===
|
||||
// IMPORTANT: Always reset sync context, even if not initialized
|
||||
// This ensures cleanup works correctly even if Shutdown is called without Init
|
||||
// Reset synchronization context to allow the user to run async/await,
|
||||
// as the main loop has been ended, the synchronization context from
|
||||
// gui.cs does no longer process any callbacks. See #1084 for more details:
|
||||
// (https://github.com/gui-cs/Terminal.Gui/issues/1084).
|
||||
SynchronizationContext.SetSynchronizationContext (null);
|
||||
|
||||
// Note: ForceDriver and Force16Colors are NOT reset;
|
||||
// they need to persist across Init/Shutdown cycles
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="InitializedChanged"/> event.
|
||||
/// </summary>
|
||||
internal void RaiseInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
|
||||
}
|
||||
347
Terminal.Gui/App/ApplicationImpl.Run.cs
Normal file
347
Terminal.Gui/App/ApplicationImpl.Run.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public partial class ApplicationImpl
|
||||
{
|
||||
/// <summary>
|
||||
/// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during
|
||||
/// <see cref="Init"/> and used to determine if code is executing on the main thread.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The managed thread ID of the main UI thread, or <see langword="null"/> if the application is not initialized.
|
||||
/// </value>
|
||||
internal int? MainThreadId { get; set; }
|
||||
|
||||
#region Begin->Run->Stop->End
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<SessionTokenEventArgs>? SessionBegun;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ToplevelEventArgs>? SessionEnded;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SessionToken Begin (Toplevel toplevel)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (toplevel);
|
||||
|
||||
// Ensure the mouse is ungrabbed.
|
||||
if (Mouse.MouseGrabView is { })
|
||||
{
|
||||
Mouse.UngrabMouse ();
|
||||
}
|
||||
|
||||
var rs = new SessionToken (toplevel);
|
||||
|
||||
#if DEBUG_IDISPOSABLE
|
||||
if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
|
||||
{
|
||||
// This assertion confirm if the Top was already disposed
|
||||
Debug.Assert (Top.WasDisposed);
|
||||
Debug.Assert (Top == CachedSessionTokenToplevel);
|
||||
}
|
||||
#endif
|
||||
|
||||
lock (TopLevels)
|
||||
{
|
||||
if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
|
||||
{
|
||||
// If Top was already disposed and isn't on the Toplevels Stack,
|
||||
// clean it up here if is the same as _CachedSessionTokenToplevel
|
||||
if (Top == CachedSessionTokenToplevel)
|
||||
{
|
||||
Top = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Probably this will never hit
|
||||
throw new ObjectDisposedException (Top.GetType ().FullName);
|
||||
}
|
||||
}
|
||||
|
||||
// BUGBUG: We should not depend on `Id` internally.
|
||||
// BUGBUG: It is super unclear what this code does anyway.
|
||||
if (string.IsNullOrEmpty (toplevel.Id))
|
||||
{
|
||||
var count = 1;
|
||||
var id = (TopLevels.Count + count).ToString ();
|
||||
|
||||
while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { })
|
||||
{
|
||||
count++;
|
||||
id = (TopLevels.Count + count).ToString ();
|
||||
}
|
||||
|
||||
toplevel.Id = (TopLevels.Count + count).ToString ();
|
||||
|
||||
TopLevels.Push (toplevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id);
|
||||
|
||||
if (dup is null)
|
||||
{
|
||||
TopLevels.Push (toplevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Top is null)
|
||||
{
|
||||
Top = toplevel;
|
||||
}
|
||||
|
||||
if ((Top?.Modal == false && toplevel.Modal)
|
||||
|| (Top?.Modal == false && !toplevel.Modal)
|
||||
|| (Top?.Modal == true && toplevel.Modal))
|
||||
{
|
||||
if (toplevel.Visible)
|
||||
{
|
||||
if (Top is { HasFocus: true })
|
||||
{
|
||||
Top.HasFocus = false;
|
||||
}
|
||||
|
||||
// Force leave events for any entered views in the old Top
|
||||
if (Mouse.GetLastMousePosition () is { })
|
||||
{
|
||||
Mouse.RaiseMouseEnterLeaveEvents (Mouse.GetLastMousePosition ()!.Value, new ());
|
||||
}
|
||||
|
||||
Top?.OnDeactivate (toplevel);
|
||||
Toplevel previousTop = Top!;
|
||||
|
||||
Top = toplevel;
|
||||
Top.OnActivate (previousTop);
|
||||
}
|
||||
}
|
||||
|
||||
// View implements ISupportInitializeNotification which is derived from ISupportInitialize
|
||||
if (!toplevel.IsInitialized)
|
||||
{
|
||||
toplevel.BeginInit ();
|
||||
toplevel.EndInit (); // Calls Layout
|
||||
}
|
||||
|
||||
// Try to set initial focus to any TabStop
|
||||
if (!toplevel.HasFocus)
|
||||
{
|
||||
toplevel.SetFocus ();
|
||||
}
|
||||
|
||||
toplevel.OnLoaded ();
|
||||
|
||||
Instance.LayoutAndDraw (true);
|
||||
|
||||
if (PositionCursor ())
|
||||
{
|
||||
Driver?.UpdateCursor ();
|
||||
}
|
||||
|
||||
SessionBegun?.Invoke (this, new (rs));
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool StopAfterFirstIteration { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<IterationEventArgs>? Iteration;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null) { return Run<Toplevel> (errorHandler, driver); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
|
||||
where TView : Toplevel, new ()
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
// Init() has NOT been called. Auto-initialize as per interface contract.
|
||||
Init (null, driver);
|
||||
}
|
||||
|
||||
TView top = new ();
|
||||
Run (top, errorHandler);
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
|
||||
{
|
||||
Logging.Information ($"Run '{view}'");
|
||||
ArgumentNullException.ThrowIfNull (view);
|
||||
|
||||
if (!Initialized)
|
||||
{
|
||||
throw new NotInitializedException (nameof (Run));
|
||||
}
|
||||
|
||||
if (Driver == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
|
||||
}
|
||||
|
||||
Top = view;
|
||||
|
||||
SessionToken rs = Application.Begin (view);
|
||||
|
||||
Top.Running = true;
|
||||
|
||||
var firstIteration = true;
|
||||
|
||||
while (TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
|
||||
{
|
||||
if (Coordinator is null)
|
||||
{
|
||||
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
|
||||
}
|
||||
|
||||
Coordinator.RunIteration ();
|
||||
|
||||
if (StopAfterFirstIteration && firstIteration)
|
||||
{
|
||||
Logging.Information ("Run - Stopping after first iteration as requested");
|
||||
view.RequestStop ();
|
||||
}
|
||||
|
||||
firstIteration = false;
|
||||
}
|
||||
|
||||
Logging.Information ("Run - Calling End");
|
||||
Application.End (rs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void End (SessionToken sessionToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull (sessionToken);
|
||||
|
||||
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
||||
{
|
||||
ApplicationPopover.HideWithQuitCommand (visiblePopover);
|
||||
}
|
||||
|
||||
sessionToken.Toplevel.OnUnloaded ();
|
||||
|
||||
// End the Session
|
||||
// First, take it off the Toplevel Stack
|
||||
if (TopLevels.TryPop (out Toplevel? topOfStack))
|
||||
{
|
||||
if (topOfStack != sessionToken.Toplevel)
|
||||
{
|
||||
// If the top of the stack is not the SessionToken.Toplevel then
|
||||
// this call to End is not balanced with the call to Begin that started the Session
|
||||
throw new ArgumentException ("End must be balanced with calls to Begin");
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that it is closing
|
||||
sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel);
|
||||
|
||||
if (TopLevels.TryPeek (out Toplevel? newTop))
|
||||
{
|
||||
Top = newTop;
|
||||
Top?.SetNeedsDraw ();
|
||||
}
|
||||
|
||||
if (sessionToken.Toplevel is { HasFocus: true })
|
||||
{
|
||||
sessionToken.Toplevel.HasFocus = false;
|
||||
}
|
||||
|
||||
if (Top is { HasFocus: false })
|
||||
{
|
||||
Top.SetFocus ();
|
||||
}
|
||||
|
||||
CachedSessionTokenToplevel = sessionToken.Toplevel;
|
||||
|
||||
sessionToken.Toplevel = null;
|
||||
sessionToken.Dispose ();
|
||||
|
||||
// BUGBUG: Why layout and draw here? This causes the screen to be cleared!
|
||||
//LayoutAndDraw (true);
|
||||
|
||||
SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RequestStop () { RequestStop (null); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RequestStop (Toplevel? top)
|
||||
{
|
||||
Logging.Trace ($"Top: '{(top is { } ? top : "null")}'");
|
||||
|
||||
top ??= Top;
|
||||
|
||||
if (top == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToplevelClosingEventArgs ev = new (top);
|
||||
top.OnClosing (ev);
|
||||
|
||||
if (ev.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
top.Running = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RaiseIteration () { Iteration?.Invoke (null, new ()); }
|
||||
|
||||
#endregion Begin->Run->Stop->End
|
||||
|
||||
#region Timeouts and Invoke
|
||||
|
||||
private readonly ITimedEvents _timedEvents = new TimedEvents ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ITimedEvents? TimedEvents => _timedEvents;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Invoke (Action action)
|
||||
{
|
||||
// If we are already on the main UI thread
|
||||
if (Top is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
|
||||
{
|
||||
action ();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_timedEvents.Add (
|
||||
TimeSpan.Zero,
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endregion Timeouts and Invoke
|
||||
}
|
||||
177
Terminal.Gui/App/ApplicationImpl.Screen.cs
Normal file
177
Terminal.Gui/App/ApplicationImpl.Screen.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
public partial class ApplicationImpl
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
|
||||
|
||||
private readonly object _lockScreen = new ();
|
||||
private Rectangle? _screen;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Rectangle Screen
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockScreen)
|
||||
{
|
||||
if (_screen == null)
|
||||
{
|
||||
_screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
|
||||
}
|
||||
|
||||
return _screen.Value;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value is { } && (value.X != 0 || value.Y != 0))
|
||||
{
|
||||
throw new NotImplementedException ("Screen locations other than 0, 0 are not yet supported");
|
||||
}
|
||||
|
||||
lock (_lockScreen)
|
||||
{
|
||||
_screen = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ClearScreenNextIteration { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PositionCursor ()
|
||||
{
|
||||
// Find the most focused view and position the cursor there.
|
||||
View? mostFocused = Navigation?.GetFocused ();
|
||||
|
||||
// If the view is not visible or enabled, don't position the cursor
|
||||
if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
|
||||
{
|
||||
var current = CursorVisibility.Invisible;
|
||||
Driver?.GetCursorVisibility (out current);
|
||||
|
||||
if (current != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver?.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the view is not visible within it's superview, don't position the cursor
|
||||
Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
|
||||
|
||||
Rectangle superViewViewport =
|
||||
mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
|
||||
|
||||
if (!superViewViewport.IntersectsWith (mostFocusedViewport))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Point? cursor = mostFocused.PositionCursor ();
|
||||
|
||||
Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility);
|
||||
|
||||
if (cursor is { })
|
||||
{
|
||||
// Convert cursor to screen coords
|
||||
cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location;
|
||||
|
||||
// If the cursor is not in a visible location in the SuperView, hide it
|
||||
if (!superViewViewport.Contains (cursor.Value))
|
||||
{
|
||||
if (currentCursorVisibility != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show it
|
||||
if (currentCursorVisibility == CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (mostFocused.CursorVisibility);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentCursorVisibility != CursorVisibility.Invisible)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Resets the Screen field to null so it will be recalculated on next access.
|
||||
/// </summary>
|
||||
private void ResetScreen ()
|
||||
{
|
||||
lock (_lockScreen)
|
||||
{
|
||||
_screen = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Called when the application's size has changed. Sets the size of all <see cref="Toplevel"/>s and fires
|
||||
/// the
|
||||
/// <see cref="ScreenChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="screen">The new screen size and position.</param>
|
||||
private void RaiseScreenChangedEvent (Rectangle screen)
|
||||
{
|
||||
Screen = new (Point.Empty, screen.Size);
|
||||
|
||||
ScreenChanged?.Invoke (this, new (screen));
|
||||
|
||||
foreach (Toplevel t in TopLevels)
|
||||
{
|
||||
t.OnSizeChanging (new (screen.Size));
|
||||
t.SetNeedsLayout ();
|
||||
}
|
||||
|
||||
LayoutAndDraw (true);
|
||||
}
|
||||
|
||||
private void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { RaiseScreenChangedEvent (new (new (0, 0), e.Size!.Value)); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void LayoutAndDraw (bool forceRedraw = false)
|
||||
{
|
||||
List<View> tops = [.. TopLevels];
|
||||
|
||||
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
||||
{
|
||||
visiblePopover.SetNeedsDraw ();
|
||||
visiblePopover.SetNeedsLayout ();
|
||||
tops.Insert (0, visiblePopover);
|
||||
}
|
||||
|
||||
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size);
|
||||
|
||||
if (ClearScreenNextIteration)
|
||||
{
|
||||
forceRedraw = true;
|
||||
ClearScreenNextIteration = false;
|
||||
}
|
||||
|
||||
if (forceRedraw)
|
||||
{
|
||||
Driver?.ClearContents ();
|
||||
}
|
||||
|
||||
View.SetClipToScreen ();
|
||||
View.Draw (tops, neededLayout || forceRedraw);
|
||||
View.SetClipToScreen ();
|
||||
Driver?.Refresh ();
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,55 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Terminal.Gui.Drivers;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of core <see cref="Application"/> methods using the modern
|
||||
/// main loop architecture with component factories for different platforms.
|
||||
/// Implementation of core <see cref="Application"/> methods using the modern
|
||||
/// main loop architecture with component factories for different platforms.
|
||||
/// </summary>
|
||||
public class ApplicationImpl : IApplication
|
||||
public partial class ApplicationImpl : IApplication
|
||||
{
|
||||
private readonly IComponentFactory? _componentFactory;
|
||||
private IMainLoopCoordinator? _coordinator;
|
||||
private string? _driverName;
|
||||
private readonly ITimedEvents _timedEvents = new TimedEvents ();
|
||||
private IConsoleDriver? _driver;
|
||||
private bool _initialized;
|
||||
private ApplicationPopover? _popover;
|
||||
private ApplicationNavigation? _navigation;
|
||||
private Toplevel? _top;
|
||||
private readonly ConcurrentStack<Toplevel> _topLevels = new ();
|
||||
private int _mainThreadId = -1;
|
||||
private bool _force16Colors;
|
||||
private string _forceDriver = string.Empty;
|
||||
private readonly List<SixelToRender> _sixel = new ();
|
||||
private readonly object _lockScreen = new ();
|
||||
private Rectangle? _screen;
|
||||
private bool _clearScreenNextIteration;
|
||||
/// <summary>
|
||||
/// Creates a new instance of the Application backend.
|
||||
/// </summary>
|
||||
public ApplicationImpl () { }
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Creates a new instance of the Application backend.
|
||||
/// </summary>
|
||||
/// <param name="componentFactory"></param>
|
||||
internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; }
|
||||
|
||||
#region Singleton
|
||||
|
||||
// Private static readonly Lazy instance of Application
|
||||
private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
|
||||
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
|
||||
/// Change the singleton implementation, should not be called except before application
|
||||
/// startup. This method lets you provide alternative implementations of core static gateway
|
||||
/// methods of <see cref="Application"/>.
|
||||
/// </summary>
|
||||
/// <param name="newApplication"></param>
|
||||
public static void ChangeInstance (IApplication? newApplication) { _lazyInstance = new (newApplication!); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
|
||||
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
|
||||
/// </summary>
|
||||
public static IApplication Instance => _lazyInstance.Value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ITimedEvents? TimedEvents => _timedEvents;
|
||||
#endregion Singleton
|
||||
|
||||
internal IMainLoopCoordinator? Coordinator => _coordinator;
|
||||
private string? _driverName;
|
||||
|
||||
|
||||
#region Input
|
||||
|
||||
private IMouse? _mouse;
|
||||
|
||||
/// <summary>
|
||||
/// Handles mouse event state and processing.
|
||||
/// Handles mouse event state and processing.
|
||||
/// </summary>
|
||||
public IMouse Mouse
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public class ApplicationImpl : IApplication
|
||||
{
|
||||
_mouse = new MouseImpl { Application = this };
|
||||
}
|
||||
|
||||
return _mouse;
|
||||
}
|
||||
set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
|
||||
@@ -66,7 +68,7 @@ public class ApplicationImpl : IApplication
|
||||
private IKeyboard? _keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Handles keyboard input and key bindings at the Application level
|
||||
/// Handles keyboard input and key bindings at the Application level
|
||||
/// </summary>
|
||||
public IKeyboard Keyboard
|
||||
{
|
||||
@@ -76,479 +78,32 @@ public class ApplicationImpl : IApplication
|
||||
{
|
||||
_keyboard = new KeyboardImpl { Application = this };
|
||||
}
|
||||
|
||||
return _keyboard;
|
||||
}
|
||||
set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IConsoleDriver? Driver
|
||||
{
|
||||
get => _driver;
|
||||
set => _driver = value;
|
||||
}
|
||||
#endregion Input
|
||||
|
||||
#region View Management
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Initialized
|
||||
{
|
||||
get => _initialized;
|
||||
set => _initialized = value;
|
||||
}
|
||||
public ApplicationPopover? Popover { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Force16Colors
|
||||
{
|
||||
get => _force16Colors;
|
||||
set => _force16Colors = value;
|
||||
}
|
||||
public ApplicationNavigation? Navigation { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<SixelToRender> Sixel => _sixel;
|
||||
public ConcurrentStack<Toplevel> TopLevels { get; } = new ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ClearScreenNextIteration
|
||||
{
|
||||
get => _clearScreenNextIteration;
|
||||
set => _clearScreenNextIteration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ApplicationPopover? Popover
|
||||
{
|
||||
get => _popover;
|
||||
set => _popover = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ApplicationNavigation? Navigation
|
||||
{
|
||||
get => _navigation;
|
||||
set => _navigation = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Toplevel? Top
|
||||
{
|
||||
get => _top;
|
||||
set => _top = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentStack<Toplevel> TopLevels => _topLevels;
|
||||
|
||||
// When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.
|
||||
// This variable is set in `End` in this case so that `Begin` correctly sets `Top`.
|
||||
/// <inheritdoc />
|
||||
public Toplevel? CachedRunStateToplevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main thread ID for the application.
|
||||
/// </summary>
|
||||
internal int MainThreadId
|
||||
{
|
||||
get => _mainThreadId;
|
||||
set => _mainThreadId = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RequestStop () => RequestStop (null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the Application backend.
|
||||
/// </summary>
|
||||
public ApplicationImpl ()
|
||||
{
|
||||
}
|
||||
|
||||
internal ApplicationImpl (IComponentFactory componentFactory)
|
||||
{
|
||||
_componentFactory = componentFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the singleton implementation, should not be called except before application
|
||||
/// startup. This method lets you provide alternative implementations of core static gateway
|
||||
/// methods of <see cref="Application"/>.
|
||||
/// </summary>
|
||||
/// <param name="newApplication"></param>
|
||||
public static void ChangeInstance (IApplication newApplication)
|
||||
{
|
||||
_lazyInstance = new Lazy<IApplication> (newApplication);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public void Init (IConsoleDriver? driver = null, string? driverName = null)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
|
||||
|
||||
throw new InvalidOperationException ("Init called multiple times without Shutdown");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace (driverName))
|
||||
{
|
||||
_driverName = driverName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace (_driverName))
|
||||
{
|
||||
_driverName = ForceDriver;
|
||||
}
|
||||
|
||||
Debug.Assert (_navigation is null);
|
||||
_navigation = new ();
|
||||
|
||||
Debug.Assert (_popover is null);
|
||||
_popover = new ();
|
||||
|
||||
// Preserve existing keyboard settings if they exist
|
||||
bool hasExistingKeyboard = _keyboard is not null;
|
||||
Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
|
||||
Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
|
||||
Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
|
||||
Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
|
||||
Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
|
||||
Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
|
||||
|
||||
// Reset keyboard to ensure fresh state with default bindings
|
||||
_keyboard = new KeyboardImpl { Application = this };
|
||||
|
||||
// Restore previously set keys if they existed and were different from defaults
|
||||
if (hasExistingKeyboard)
|
||||
{
|
||||
_keyboard.QuitKey = existingQuitKey;
|
||||
_keyboard.ArrangeKey = existingArrangeKey;
|
||||
_keyboard.NextTabKey = existingNextTabKey;
|
||||
_keyboard.PrevTabKey = existingPrevTabKey;
|
||||
_keyboard.NextTabGroupKey = existingNextTabGroupKey;
|
||||
_keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
|
||||
}
|
||||
|
||||
CreateDriver (driverName ?? _driverName);
|
||||
Screen = Driver!.Screen;
|
||||
_initialized = true;
|
||||
|
||||
Application.OnInitializedChanged (this, new (true));
|
||||
Application.SubscribeDriverEvents ();
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext (new ());
|
||||
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
}
|
||||
|
||||
private void CreateDriver (string? driverName)
|
||||
{
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
// Check component factory type first - this takes precedence over driverName
|
||||
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
|
||||
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
|
||||
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
|
||||
|
||||
// Then check driverName
|
||||
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
|
||||
// Decide which driver to use - component factory type takes priority
|
||||
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
|
||||
{
|
||||
FakeConsoleOutput fakeOutput = new ();
|
||||
fakeOutput.SetConsoleSize (80, 25);
|
||||
|
||||
_coordinator = CreateSubcomponents (() => new FakeComponentFactory (null, fakeOutput));
|
||||
}
|
||||
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new NetComponentFactory ());
|
||||
}
|
||||
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
}
|
||||
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
|
||||
}
|
||||
else
|
||||
{
|
||||
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
|
||||
}
|
||||
|
||||
_coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (_driver == null)
|
||||
{
|
||||
throw new ("Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
}
|
||||
|
||||
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
|
||||
{
|
||||
ConcurrentQueue<T> inputBuffer = new ();
|
||||
ApplicationMainLoop<T> loop = new ();
|
||||
|
||||
IComponentFactory<T> cf;
|
||||
|
||||
if (_componentFactory is IComponentFactory<T> typedFactory)
|
||||
{
|
||||
cf = typedFactory;
|
||||
}
|
||||
else
|
||||
{
|
||||
cf = fallbackFactory ();
|
||||
}
|
||||
|
||||
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/> object and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <param name="errorHandler"></param>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
|
||||
/// be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
|
||||
/// </param>
|
||||
/// <returns>The created T object. The caller is responsible for disposing this object.</returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
where T : Toplevel, new()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
// Init() has NOT been called. Auto-initialize as per interface contract.
|
||||
Init (driver, null);
|
||||
}
|
||||
|
||||
T top = new ();
|
||||
Run (top, errorHandler);
|
||||
return top;
|
||||
}
|
||||
|
||||
/// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
|
||||
/// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
|
||||
/// <param name="errorHandler">Handler for any unhandled exceptions.</param>
|
||||
public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
|
||||
{
|
||||
Logging.Information ($"Run '{view}'");
|
||||
ArgumentNullException.ThrowIfNull (view);
|
||||
|
||||
if (!_initialized)
|
||||
{
|
||||
throw new NotInitializedException (nameof (Run));
|
||||
}
|
||||
|
||||
if (_driver == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
|
||||
}
|
||||
|
||||
_top = view;
|
||||
|
||||
RunState rs = Application.Begin (view);
|
||||
|
||||
_top.Running = true;
|
||||
|
||||
while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
|
||||
{
|
||||
if (_coordinator is null)
|
||||
{
|
||||
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
|
||||
}
|
||||
|
||||
_coordinator.RunIteration ();
|
||||
}
|
||||
|
||||
Logging.Information ($"Run - Calling End");
|
||||
Application.End (rs);
|
||||
}
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
public void Shutdown ()
|
||||
{
|
||||
_coordinator?.Stop ();
|
||||
|
||||
bool wasInitialized = _initialized;
|
||||
|
||||
// Reset Screen before calling Application.ResetState to avoid circular reference
|
||||
ResetScreen ();
|
||||
|
||||
// Call ResetState FIRST so it can properly dispose Popover and other resources
|
||||
// that are accessed via Application.* static properties that now delegate to instance fields
|
||||
Application.ResetState ();
|
||||
ConfigurationManager.PrintJsonErrors ();
|
||||
|
||||
// Clear instance fields after ResetState has disposed everything
|
||||
_driver = null;
|
||||
_mouse = null;
|
||||
_keyboard = null;
|
||||
_initialized = false;
|
||||
_navigation = null;
|
||||
_popover = null;
|
||||
CachedRunStateToplevel = null;
|
||||
_top = null;
|
||||
_topLevels.Clear ();
|
||||
_mainThreadId = -1;
|
||||
_screen = null;
|
||||
_clearScreenNextIteration = false;
|
||||
_sixel.Clear ();
|
||||
// Don't reset ForceDriver and Force16Colors; they need to be set before Init is called
|
||||
|
||||
if (wasInitialized)
|
||||
{
|
||||
bool init = _initialized; // Will be false after clearing fields above
|
||||
Application.OnInitializedChanged (this, new (in init));
|
||||
}
|
||||
|
||||
_lazyInstance = new (() => new ApplicationImpl ());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RequestStop (Toplevel? top)
|
||||
{
|
||||
Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");
|
||||
|
||||
top ??= _top;
|
||||
|
||||
if (top == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToplevelClosingEventArgs ev = new (top);
|
||||
top.OnClosing (ev);
|
||||
|
||||
if (ev.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
top.Running = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Invoke (Action action)
|
||||
{
|
||||
// If we are already on the main UI thread
|
||||
if (Top is { Running: true } && _mainThreadId == Thread.CurrentThread.ManagedThreadId)
|
||||
{
|
||||
action ();
|
||||
return;
|
||||
}
|
||||
|
||||
_timedEvents.Add (TimeSpan.Zero,
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLegacy => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LayoutAndDraw (bool forceRedraw = false)
|
||||
{
|
||||
List<View> tops = [.. _topLevels];
|
||||
|
||||
if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
|
||||
{
|
||||
visiblePopover.SetNeedsDraw ();
|
||||
visiblePopover.SetNeedsLayout ();
|
||||
tops.Insert (0, visiblePopover);
|
||||
}
|
||||
|
||||
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size);
|
||||
|
||||
if (ClearScreenNextIteration)
|
||||
{
|
||||
forceRedraw = true;
|
||||
ClearScreenNextIteration = false;
|
||||
}
|
||||
|
||||
if (forceRedraw)
|
||||
{
|
||||
_driver?.ClearContents ();
|
||||
}
|
||||
|
||||
View.SetClipToScreen ();
|
||||
View.Draw (tops, neededLayout || forceRedraw);
|
||||
View.SetClipToScreen ();
|
||||
_driver?.Refresh ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Screen field to null so it will be recalculated on next access.
|
||||
/// </summary>
|
||||
internal void ResetScreen ()
|
||||
{
|
||||
lock (_lockScreen)
|
||||
{
|
||||
_screen = null;
|
||||
}
|
||||
}
|
||||
#endregion View Management
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public abstract class ClipboardBase : IClipboard
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the contents of the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/>-specific
|
||||
/// Returns the contents of the OS clipboard if possible. Implemented by <see cref="IDriver"/>-specific
|
||||
/// subclasses.
|
||||
/// </summary>
|
||||
/// <returns>The contents of the OS clipboard if successful.</returns>
|
||||
@@ -111,7 +111,7 @@ public abstract class ClipboardBase : IClipboard
|
||||
protected abstract string GetClipboardDataImpl ();
|
||||
|
||||
/// <summary>
|
||||
/// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/>
|
||||
/// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="IDriver"/>
|
||||
/// -specific subclasses.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to paste to the OS clipboard.</param>
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by
|
||||
/// UnixDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs.
|
||||
/// UnixDriver, but also used in Unit tests which is why it is in IDriver.cs.
|
||||
/// </summary>
|
||||
internal static class ClipboardProcessRunner
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public interface IApplication
|
||||
{
|
||||
/// <summary>Adds a timeout to the application.</summary>
|
||||
/// <remarks>
|
||||
/// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
|
||||
/// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
|
||||
/// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
|
||||
/// </remarks>
|
||||
object AddTimeout (TimeSpan time, Func<bool> callback);
|
||||
#region Keyboard
|
||||
|
||||
/// <summary>
|
||||
/// Handles keyboard input and key bindings at the Application level.
|
||||
/// Handles keyboard input and key bindings at the Application level.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Provides access to keyboard state, key bindings, and keyboard event handling. Set during <see cref="Init"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
IKeyboard Keyboard { get; set; }
|
||||
|
||||
#endregion Keyboard
|
||||
|
||||
#region Mouse
|
||||
|
||||
/// <summary>
|
||||
/// Handles mouse event state and processing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Provides access to mouse state, mouse grabbing, and mouse event handling. Set during <see cref="Init"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
IMouse Mouse { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the console driver being used.</summary>
|
||||
IConsoleDriver? Driver { get; set; }
|
||||
#endregion Mouse
|
||||
|
||||
#region Initialization and Shutdown
|
||||
|
||||
/// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or
|
||||
/// <paramref name="driverName"/> are specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
/// <param name="driverName">
|
||||
/// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the
|
||||
/// <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
|
||||
/// specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
|
||||
/// <para>
|
||||
/// This function loads the right <see cref="IDriver"/> for the platform, creates a main loop coordinator,
|
||||
/// initializes keyboard and mouse handlers, and subscribes to driver events.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after
|
||||
/// <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="Run{T}"/> function combines <see cref="Init(IDriver,string)"/> and
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/> into a single call. An application can use
|
||||
/// <see cref="Run{T}"/> without explicitly calling <see cref="Init(IDriver,string)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public void Init (IDriver? driver = null, string? driverName = null);
|
||||
|
||||
/// <summary>
|
||||
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intended to support unit tests that need to know when the application has been initialized.
|
||||
/// </remarks>
|
||||
public event EventHandler<EventArgs<bool>>? InitializedChanged;
|
||||
|
||||
/// <summary>Gets or sets whether the application has been initialized.</summary>
|
||||
bool Initialized { get; set; }
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Shutdown must be called for every call to <see cref="Init"/> or
|
||||
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
|
||||
/// up (Disposed) and terminal settings are restored.
|
||||
/// </remarks>
|
||||
public void Shutdown ();
|
||||
|
||||
/// <summary>
|
||||
/// Resets the state of this instance.
|
||||
/// </summary>
|
||||
/// <param name="ignoreDisposed">If true, ignores disposed state checks during reset.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Encapsulates all setting of initial state for Application; having this in a function like this ensures we
|
||||
/// don't make mistakes in guaranteeing that the state of this singleton is deterministic when <see cref="Init"/>
|
||||
/// starts running and after <see cref="Shutdown"/> returns.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void ResetState (bool ignoreDisposed = false);
|
||||
|
||||
#endregion Initialization and Shutdown
|
||||
|
||||
#region Begin->Run->Iteration->Stop->End
|
||||
|
||||
/// <summary>
|
||||
/// Building block API: Creates a <see cref="SessionToken"/> and prepares the provided <see cref="Toplevel"/> for
|
||||
/// execution. Not usually called directly by applications. Use <see cref="Run(Toplevel, Func{Exception, bool})"/>
|
||||
/// instead.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The <see cref="SessionToken"/> that needs to be passed to the <see cref="End(SessionToken)"/> method upon
|
||||
/// completion.
|
||||
/// </returns>
|
||||
/// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method prepares the provided <see cref="Toplevel"/> for running. It adds this to the
|
||||
/// list of <see cref="Toplevel"/>s, lays out the SubViews, focuses the first element, and draws the
|
||||
/// <see cref="Toplevel"/> on the screen. This is usually followed by starting the main loop, and then the
|
||||
/// <see cref="End(SessionToken)"/> method upon termination which will undo these changes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Raises the <see cref="SessionBegun"/> event before returning.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public SessionToken Begin (Toplevel toplevel);
|
||||
|
||||
/// <summary>
|
||||
/// Runs a new Session creating a <see cref="Toplevel"/> and calling <see cref="Begin(Toplevel)"/>. When the session is
|
||||
/// stopped, <see cref="End(SessionToken)"/> will be called.
|
||||
/// </summary>
|
||||
/// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
|
||||
/// <param name="driver">
|
||||
/// The driver name. If not specified the default driver for the platform will be used. Must be
|
||||
/// <see langword="null"/> if <see cref="Init"/> has already been called.
|
||||
/// </param>
|
||||
/// <returns>The created <see cref="Toplevel"/>. The caller is responsible for disposing this object.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null);
|
||||
|
||||
/// <summary>
|
||||
/// Runs a new Session creating a <see cref="Toplevel"/>-derived object of type <typeparamref name="TView"/>
|
||||
/// and calling <see cref="Run(Toplevel, Func{Exception, bool})"/>. When the session is stopped,
|
||||
/// <see cref="End(SessionToken)"/> will be called.
|
||||
/// </summary>
|
||||
/// <typeparam name="TView">The type of <see cref="Toplevel"/> to create and run.</typeparam>
|
||||
/// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
|
||||
/// <param name="driver">
|
||||
/// The driver name. If not specified the default driver for the platform will be used. Must be
|
||||
/// <see langword="null"/> if <see cref="Init"/> has already been called.
|
||||
/// </param>
|
||||
/// <returns>The created <typeparamref name="TView"/> object. The caller is responsible for disposing this object.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is used to start processing events for the main application, but it is also used to run other
|
||||
/// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To make <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call
|
||||
/// <see cref="RequestStop()"/> or <see cref="RequestStop(Toplevel)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling
|
||||
/// <see cref="Begin(Toplevel)"/>, followed by starting the main loop, and then calling
|
||||
/// <see cref="End(SessionToken)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When using <see cref="Run{T}"/> or <see cref="Run(Func{Exception, bool}, string)"/>,
|
||||
/// <see cref="Init"/> will be called automatically.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
|
||||
/// rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
|
||||
/// returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
|
||||
/// rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
|
||||
/// returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
|
||||
where TView : Toplevel, new ();
|
||||
|
||||
/// <summary>
|
||||
/// Runs a new Session using the provided <see cref="Toplevel"/> view and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// When the session is stopped, <see cref="End(SessionToken)"/> will be called..
|
||||
/// </summary>
|
||||
/// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
|
||||
/// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is used to start processing events for the main application, but it is also used to run other
|
||||
/// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To make <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call
|
||||
/// <see cref="RequestStop()"/> or <see cref="RequestStop(Toplevel)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling
|
||||
/// <see cref="Begin(Toplevel)"/>, followed by starting the main loop, and then calling
|
||||
/// <see cref="End(SessionToken)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When using <see cref="Run{T}"/> or <see cref="Run(Func{Exception, bool}, string)"/>,
|
||||
/// <see cref="Init"/> will be called automatically.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
|
||||
/// rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
|
||||
/// returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="Iteration"/> event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called once per main loop iteration, before processing input, timeouts, or rendering.
|
||||
/// </remarks>
|
||||
public void RaiseIteration ();
|
||||
|
||||
/// <summary>This event is raised on each iteration of the main loop.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This event is raised before input processing, timeout callbacks, and rendering occur each iteration.
|
||||
/// </para>
|
||||
/// <para>See also <see cref="AddTimeout"/> and <see cref="TimedEvents"/>.</para>
|
||||
/// </remarks>
|
||||
public event EventHandler<IterationEventArgs>? Iteration;
|
||||
|
||||
/// <summary>Runs <paramref name="action"/> on the main UI loop thread.</summary>
|
||||
/// <param name="action">The action to be invoked on the main processing thread.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If called from the main thread, the action is executed immediately. Otherwise, it is queued via
|
||||
/// <see cref="AddTimeout"/> with <see cref="TimeSpan.Zero"/> and will be executed on the next main loop
|
||||
/// iteration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void Invoke (Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Building block API: Ends a Session and completes the execution of a <see cref="Toplevel"/> that was started with
|
||||
/// <see cref="Begin(Toplevel)"/>. Not usually called directly by applications.
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>
|
||||
/// will automatically call this method when the session is stopped.
|
||||
/// </summary>
|
||||
/// <param name="sessionToken">The <see cref="SessionToken"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method removes the <see cref="Toplevel"/> from the stack, raises the <see cref="SessionEnded"/>
|
||||
/// event, and disposes the <paramref name="sessionToken"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void End (SessionToken sessionToken);
|
||||
|
||||
/// <summary>Requests that the currently running Session stop. The Session will stop after the current iteration completes.</summary>
|
||||
/// <remarks>
|
||||
/// <para>This will cause <see cref="Run(Toplevel, Func{Exception, bool})"/> to return.</para>
|
||||
/// <para>
|
||||
/// This is equivalent to calling <see cref="RequestStop(Toplevel)"/> with <see cref="Top"/> as the parameter.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void RequestStop ();
|
||||
|
||||
/// <summary>Requests that the currently running Session stop. The Session will stop after the current iteration completes.</summary>
|
||||
/// <param name="top">
|
||||
/// The <see cref="Toplevel"/> to stop. If <see langword="null"/>, stops the currently running <see cref="Top"/>.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>This will cause <see cref="Run(Toplevel, Func{Exception, bool})"/> to return.</para>
|
||||
/// <para>
|
||||
/// Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
|
||||
/// property on the specified <see cref="Toplevel"/> to <see langword="false"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void RequestStop (Toplevel? top);
|
||||
|
||||
/// <summary>
|
||||
/// Set to <see langword="true"/> to cause the session to stop running after first iteration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Used primarily for unit testing. When <see langword="true"/>, <see cref="End"/> will be called
|
||||
/// automatically after the first main loop iteration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool StopAfterFirstIteration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when <see cref="Begin(Toplevel)"/> has been called and has created a new <see cref="SessionToken"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="StopAfterFirstIteration"/> is <see langword="true"/>, callers to <see cref="Begin(Toplevel)"/>
|
||||
/// must also subscribe to <see cref="SessionEnded"/> and manually dispose of the <see cref="SessionToken"/> token
|
||||
/// when the application is done.
|
||||
/// </remarks>
|
||||
public event EventHandler<SessionTokenEventArgs>? SessionBegun;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when <see cref="End(SessionToken)"/> was called and the session is stopping. The event args contain a
|
||||
/// reference to the <see cref="Toplevel"/>
|
||||
/// that was active during the session. This can be used to ensure the Toplevel is disposed of properly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="StopAfterFirstIteration"/> is <see langword="true"/>, callers to <see cref="Begin(Toplevel)"/>
|
||||
/// must also subscribe to <see cref="SessionEnded"/> and manually dispose of the <see cref="SessionToken"/> token
|
||||
/// when the application is done.
|
||||
/// </remarks>
|
||||
public event EventHandler<ToplevelEventArgs>? SessionEnded;
|
||||
|
||||
#endregion Begin->Run->Iteration->Stop->End
|
||||
|
||||
#region Toplevel Management
|
||||
|
||||
/// <summary>Gets or sets the current Toplevel.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is set by <see cref="Begin(Toplevel)"/> and cleared by <see cref="End(SessionToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Toplevel? Top { get; set; }
|
||||
|
||||
/// <summary>Gets the stack of all Toplevels.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Toplevels are added to this stack by <see cref="Begin(Toplevel)"/> and removed by
|
||||
/// <see cref="End(SessionToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ConcurrentStack<Toplevel> TopLevels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Caches the Toplevel associated with the current Session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used internally to optimize Toplevel state transitions.
|
||||
/// </remarks>
|
||||
Toplevel? CachedSessionTokenToplevel { get; set; }
|
||||
|
||||
#endregion Toplevel Management
|
||||
|
||||
#region Screen and Driver
|
||||
|
||||
/// <summary>Gets or sets the console driver being used.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Set by <see cref="Init"/> based on the driver parameter or platform default.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
IDriver? Driver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether <see cref="Driver"/> will be forced to output only the 16 colors defined in
|
||||
/// <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
|
||||
/// as long as the selected <see cref="IConsoleDriver"/> supports TrueColor.
|
||||
/// <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be
|
||||
/// output as long as the selected <see cref="IDriver"/> supports TrueColor.
|
||||
/// </summary>
|
||||
bool Force16Colors { get; set; }
|
||||
|
||||
@@ -47,219 +402,141 @@ public interface IApplication
|
||||
string ForceDriver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of sixel images to write out to screen when updating.
|
||||
/// Only add to this collection if you are sure terminal supports sixel format.
|
||||
/// </summary>
|
||||
List<SixelToRender> Sixel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>.
|
||||
/// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the
|
||||
/// <see cref="IDriver"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If the <see cref="IDriver"/> has not been initialized, this will return a default size of 2048x2048; useful
|
||||
/// for unit tests.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Rectangle Screen { get; set; }
|
||||
|
||||
/// <summary>Raised when the terminal's size changed. The new size of the terminal is provided.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This event is raised when the driver detects a screen size change. The event provides the new screen
|
||||
/// rectangle.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is typically set to <see langword="true"/> when a View's <see cref="View.Frame"/> changes and that view
|
||||
/// has no SuperView (e.g. when <see cref="Top"/> is moved or resized).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Automatically reset to <see langword="false"/> after <see cref="LayoutAndDraw"/> processes it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool ClearScreenNextIteration { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the popover manager.</summary>
|
||||
ApplicationPopover? Popover { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the navigation manager.</summary>
|
||||
ApplicationNavigation? Navigation { get; set; }
|
||||
|
||||
/// <summary>Gets the currently active Toplevel.</summary>
|
||||
Toplevel? Top { get; set; }
|
||||
|
||||
/// <summary>Gets the stack of all Toplevels.</summary>
|
||||
System.Collections.Concurrent.ConcurrentStack<Toplevel> TopLevels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Toplevel? CachedRunStateToplevel { get; set; }
|
||||
List<SixelToRender> Sixel { get; }
|
||||
|
||||
/// <summary>Requests that the application stop running.</summary>
|
||||
void RequestStop ();
|
||||
#endregion Screen and Driver
|
||||
|
||||
#region Layout and Drawing
|
||||
|
||||
/// <summary>
|
||||
/// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
|
||||
/// need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
|
||||
/// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
|
||||
/// Causes any Toplevels that need layout to be laid out, then draws any Toplevels that need display. Only Views
|
||||
/// that need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out. Only Views that need to be drawn
|
||||
/// (see <see cref="View.NeedsDraw"/>) will be drawn.
|
||||
/// </summary>
|
||||
/// <param name="forceRedraw">
|
||||
/// If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
|
||||
/// should only be overriden for testing.
|
||||
/// should only be overridden for testing.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called automatically each main loop iteration when any views need layout or drawing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <see cref="ClearScreenNextIteration"/> is <see langword="true"/>, the screen will be cleared before
|
||||
/// drawing and the flag will be reset to <see langword="false"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void LayoutAndDraw (bool forceRedraw = false);
|
||||
|
||||
/// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
|
||||
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
|
||||
/// <para>
|
||||
/// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
|
||||
/// assigns it to <see cref="Application.Top"/>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after
|
||||
/// <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and
|
||||
/// terminal settings
|
||||
/// restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="Run{T}"/> function combines
|
||||
/// <see cref="Init(IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
|
||||
/// into a single
|
||||
/// call. An application cam use <see cref="Run{T}"/> without explicitly calling
|
||||
/// <see cref="Init(IConsoleDriver,string)"/>.
|
||||
/// </para>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or
|
||||
/// <paramref name="driverName"/> are specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
/// <param name="driverName">
|
||||
/// The driver name (e.g. "dotnet", "windows", "fake", or "unix") of the
|
||||
/// <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
|
||||
/// specified the default driver for the platform will be used.
|
||||
/// </param>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public void Init (IConsoleDriver? driver = null, string? driverName = null);
|
||||
|
||||
/// <summary>Runs <paramref name="action"/> on the main UI loop thread</summary>
|
||||
/// <param name="action">the action to be invoked on the main processing thread.</param>
|
||||
void Invoke (Action action);
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> if implementation is 'old'. <see langword="false"/> if implementation
|
||||
/// is cutting edge.
|
||||
/// Calls <see cref="View.PositionCursor"/> on the most focused view.
|
||||
/// </summary>
|
||||
bool IsLegacy { get; }
|
||||
/// <remarks>
|
||||
/// <para>Does nothing if there is no most focused view.</para>
|
||||
/// <para>
|
||||
/// If the most focused view is not visible within its superview, the cursor will be hidden.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
|
||||
public bool PositionCursor ();
|
||||
|
||||
/// <summary>Removes a previously scheduled timeout</summary>
|
||||
/// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
|
||||
#endregion Layout and Drawing
|
||||
|
||||
#region Navigation and Popover
|
||||
|
||||
/// <summary>Gets or sets the popover manager.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Manages application-level popover views. Initialized during <see cref="Init"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ApplicationPopover? Popover { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the navigation manager.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Manages focus navigation and tracking of the most focused view. Initialized during <see cref="Init"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ApplicationNavigation? Navigation { get; set; }
|
||||
|
||||
#endregion Navigation and Popover
|
||||
|
||||
#region Timeouts
|
||||
|
||||
/// <summary>Adds a timeout to the application.</summary>
|
||||
/// <param name="time">The time span to wait before invoking the callback.</param>
|
||||
/// <param name="callback">
|
||||
/// The callback to invoke. If it returns <see langword="true"/>, the timeout will be reset and repeat. If it
|
||||
/// returns <see langword="false"/>, the timeout will stop and be removed.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/>
|
||||
/// if the timeout is successfully removed; otherwise,
|
||||
/// <see langword="false"/>
|
||||
/// .
|
||||
/// This method also returns
|
||||
/// <see langword="false"/>
|
||||
/// if the timeout is not found.
|
||||
/// A token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the time specified passes, the callback will be invoked on the main UI thread.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
object AddTimeout (TimeSpan time, Func<bool> callback);
|
||||
|
||||
/// <summary>Removes a previously scheduled timeout.</summary>
|
||||
/// <param name="token">The token returned by <see cref="AddTimeout"/>.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the timeout is successfully removed; otherwise, <see langword="false"/>.
|
||||
/// This method also returns <see langword="false"/> if the timeout is not found.
|
||||
/// </returns>
|
||||
bool RemoveTimeout (object token);
|
||||
|
||||
/// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
|
||||
/// <param name="top">The <see cref="Toplevel"/> to stop.</param>
|
||||
/// <remarks>
|
||||
/// <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
|
||||
/// <para>
|
||||
/// Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
|
||||
/// property on the currently running <see cref="Toplevel"/> to false.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void RequestStop (Toplevel? top);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/> object and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
|
||||
/// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
|
||||
/// <para>
|
||||
/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
|
||||
/// ensure resources are cleaned up and terminal settings restored.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller is responsible for disposing the object returned by this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="errorHandler"></param>
|
||||
/// <param name="driver">
|
||||
/// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
|
||||
/// be used. Must be
|
||||
/// <see langword="null"/> if <see cref="Init"/> has already been called.
|
||||
/// </param>
|
||||
/// <returns>The created T object. The caller is responsible for disposing this object.</returns>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
where T : Toplevel, new();
|
||||
|
||||
/// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is used to start processing events for the main application, but it is also used to run other
|
||||
/// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To make a <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
|
||||
/// <see cref="Application.RequestStop"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
|
||||
/// <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then
|
||||
/// calling
|
||||
/// <see cref="Application.End(RunState)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Alternatively, to have a program control the main loop and process events manually, call
|
||||
/// <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call
|
||||
/// <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
|
||||
/// <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers handlers and
|
||||
/// then
|
||||
/// return control immediately.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When using <see cref="Run{T}"/> or
|
||||
/// <see cref="Run(System.Func{System.Exception,bool},IConsoleDriver)"/>
|
||||
/// <see cref="Init"/> will be called automatically.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
|
||||
/// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
|
||||
/// returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this
|
||||
/// method will
|
||||
/// exit.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
|
||||
/// <param name="errorHandler">
|
||||
/// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
|
||||
/// rethrows when null).
|
||||
/// </param>
|
||||
public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null);
|
||||
|
||||
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Shutdown must be called for every call to <see cref="Init"/> or
|
||||
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
|
||||
/// up (Disposed)
|
||||
/// and terminal settings are restored.
|
||||
/// </remarks>
|
||||
public void Shutdown ();
|
||||
|
||||
/// <summary>
|
||||
/// Handles recurring events. These are invoked on the main UI thread - allowing for
|
||||
/// safe updates to <see cref="View"/> instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Provides low-level access to the timeout management system. Most applications should use
|
||||
/// <see cref="AddTimeout"/> and <see cref="RemoveTimeout"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ITimedEvents? TimedEvents { get; }
|
||||
|
||||
#endregion Timeouts
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>Event arguments for the <see cref="Application.Iteration"/> event.</summary>
|
||||
/// <summary>Event arguments for the <see cref="IApplication.Iteration"/> event.</summary>
|
||||
public class IterationEventArgs : EventArgs
|
||||
{ }
|
||||
|
||||
@@ -17,7 +17,7 @@ public interface IKeyboard
|
||||
IApplication? Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
|
||||
/// Called when the user presses a key (by the <see cref="IDriver"/>). Raises the cancelable
|
||||
/// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
|
||||
/// if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
|
||||
/// </summary>
|
||||
@@ -27,7 +27,7 @@ public interface IKeyboard
|
||||
bool RaiseKeyDownEvent (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
|
||||
/// Called when the user releases a key (by the <see cref="IDriver"/>). Raises the cancelable
|
||||
/// <see cref="KeyUp"/>
|
||||
/// event
|
||||
/// then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>
|
||||
@@ -114,7 +116,8 @@ internal class KeyboardImpl : IKeyboard
|
||||
/// <inheritdoc/>
|
||||
public bool RaiseKeyDownEvent (Key key)
|
||||
{
|
||||
Logging.Debug ($"{key}");
|
||||
//ebug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
|
||||
//Logging.Debug ($"{key}");
|
||||
|
||||
// TODO: Add a way to ignore certain keys, esp for debugging.
|
||||
//#if DEBUG
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#nullable enable
|
||||
using Terminal.Gui.Drivers;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Terminal.Gui.Drivers;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
@@ -19,15 +20,15 @@ namespace Terminal.Gui.App;
|
||||
/// <item>Throttling iterations to respect <see cref="Application.MaximumIterationsPerSecond"/></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
|
||||
public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
/// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
|
||||
public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputRecord> where TInputRecord : struct
|
||||
{
|
||||
private ITimedEvents? _timedEvents;
|
||||
private ConcurrentQueue<T>? _inputBuffer;
|
||||
private ConcurrentQueue<TInputRecord>? _inputQueue;
|
||||
private IInputProcessor? _inputProcessor;
|
||||
private IConsoleOutput? _out;
|
||||
private IOutput? _output;
|
||||
private AnsiRequestScheduler? _ansiRequestScheduler;
|
||||
private IConsoleSizeMonitor? _consoleSizeMonitor;
|
||||
private ISizeMonitor? _sizeMonitor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ITimedEvents TimedEvents
|
||||
@@ -40,13 +41,13 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
|
||||
/// <summary>
|
||||
/// The input events thread-safe collection. This is populated on separate
|
||||
/// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each
|
||||
/// <see cref="Iteration"/>
|
||||
/// thread by a <see cref="IInput{T}"/>. Is drained as part of each
|
||||
/// <see cref="Iteration"/> on the main loop thread.
|
||||
/// </summary>
|
||||
public ConcurrentQueue<T> InputBuffer
|
||||
public ConcurrentQueue<TInputRecord> InputQueue
|
||||
{
|
||||
get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer));
|
||||
private set => _inputBuffer = value;
|
||||
get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue));
|
||||
private set => _inputQueue = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -57,13 +58,13 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IOutputBuffer OutputBuffer { get; } = new OutputBuffer ();
|
||||
public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IConsoleOutput Out
|
||||
public IOutput Output
|
||||
{
|
||||
get => _out ?? throw new NotInitializedException (nameof (Out));
|
||||
private set => _out = value;
|
||||
get => _output ?? throw new NotInitializedException (nameof (Output));
|
||||
private set => _output = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -74,10 +75,10 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IConsoleSizeMonitor ConsoleSizeMonitor
|
||||
public ISizeMonitor SizeMonitor
|
||||
{
|
||||
get => _consoleSizeMonitor ?? throw new NotInitializedException (nameof (ConsoleSizeMonitor));
|
||||
private set => _consoleSizeMonitor = value;
|
||||
get => _sizeMonitor ?? throw new NotInitializedException (nameof (SizeMonitor));
|
||||
private set => _sizeMonitor = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,12 +86,6 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
/// </summary>
|
||||
public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager ();
|
||||
|
||||
/// <summary>
|
||||
/// Determines how to get the current system type, adjust
|
||||
/// in unit tests to simulate specific timings.
|
||||
/// </summary>
|
||||
public Func<DateTime> Now { get; set; } = () => DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the class with the provided subcomponents
|
||||
/// </summary>
|
||||
@@ -101,34 +96,34 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
/// <param name="componentFactory"></param>
|
||||
public void Initialize (
|
||||
ITimedEvents timedEvents,
|
||||
ConcurrentQueue<T> inputBuffer,
|
||||
ConcurrentQueue<TInputRecord> inputBuffer,
|
||||
IInputProcessor inputProcessor,
|
||||
IConsoleOutput consoleOutput,
|
||||
IComponentFactory<T> componentFactory
|
||||
IOutput consoleOutput,
|
||||
IComponentFactory<TInputRecord> componentFactory
|
||||
)
|
||||
{
|
||||
InputBuffer = inputBuffer;
|
||||
Out = consoleOutput;
|
||||
InputQueue = inputBuffer;
|
||||
Output = consoleOutput;
|
||||
InputProcessor = inputProcessor;
|
||||
|
||||
TimedEvents = timedEvents;
|
||||
AnsiRequestScheduler = new (InputProcessor.GetParser ());
|
||||
|
||||
ConsoleSizeMonitor = componentFactory.CreateConsoleSizeMonitor (Out, OutputBuffer);
|
||||
OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height);
|
||||
SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Iteration ()
|
||||
{
|
||||
|
||||
Application.RaiseIteration ();
|
||||
|
||||
DateTime dt = Now ();
|
||||
DateTime dt = DateTime.Now;
|
||||
int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond);
|
||||
|
||||
IterationImpl ();
|
||||
|
||||
TimeSpan took = Now () - dt;
|
||||
TimeSpan took = DateTime.Now - dt;
|
||||
TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took;
|
||||
|
||||
Logging.TotalIterationMetric.Record (took.Milliseconds);
|
||||
@@ -141,6 +136,7 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
|
||||
internal void IterationImpl ()
|
||||
{
|
||||
// Pull any input events from the input queue and process them
|
||||
InputProcessor.ProcessQueue ();
|
||||
|
||||
ToplevelTransitionManager.RaiseReadyEventIfNeeded ();
|
||||
@@ -152,7 +148,7 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
|| AnySubViewsNeedDrawn (Application.Top)
|
||||
|| (Application.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (Application.Mouse.MouseGrabView));
|
||||
|
||||
bool sizeChanged = ConsoleSizeMonitor.Poll ();
|
||||
bool sizeChanged = SizeMonitor.Poll ();
|
||||
|
||||
if (needsDrawOrLayout || sizeChanged)
|
||||
{
|
||||
@@ -160,9 +156,9 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
|
||||
Application.LayoutAndDraw (true);
|
||||
|
||||
Out.Write (OutputBuffer);
|
||||
Output.Write (OutputBuffer);
|
||||
|
||||
Out.SetCursorVisibility (CursorVisibility.Default);
|
||||
Output.SetCursorVisibility (CursorVisibility.Default);
|
||||
}
|
||||
|
||||
SetCursor ();
|
||||
@@ -191,12 +187,12 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
|
||||
// Translate to screen coordinates
|
||||
to = mostFocused.ViewportToScreen (to.Value);
|
||||
|
||||
Out.SetCursorPosition (to.Value.X, to.Value.Y);
|
||||
Out.SetCursorVisibility (mostFocused.CursorVisibility);
|
||||
Output.SetCursorPosition (to.Value.X, to.Value.Y);
|
||||
Output.SetCursorVisibility (mostFocused.CursorVisibility);
|
||||
}
|
||||
else
|
||||
{
|
||||
Out.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
Output.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,27 +15,27 @@ namespace Terminal.Gui.App;
|
||||
/// <item>Rendering UI updates to the console</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">Type of raw input events processed by the loop, e.g. <see cref="ConsoleKeyInfo"/> for cross-platform .NET driver</typeparam>
|
||||
public interface IApplicationMainLoop<T> : IDisposable
|
||||
/// <typeparam name="TInputRecord">Type of raw input events processed by the loop, e.g. <see cref="ConsoleKeyInfo"/> for cross-platform .NET driver</typeparam>
|
||||
public interface IApplicationMainLoop<TInputRecord> : IDisposable where TInputRecord : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the class responsible for servicing user timeouts
|
||||
/// Gets the <see cref="ITimedEvents"/> implementation that manages user-defined timeouts and periodic events.
|
||||
/// </summary>
|
||||
public ITimedEvents TimedEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class responsible for writing final rendered output to the console
|
||||
/// Gets the <see cref="IOutputBuffer"/> representing the desired screen state for console rendering.
|
||||
/// </summary>
|
||||
public IOutputBuffer OutputBuffer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Class for writing output to the console.
|
||||
/// Gets the <see cref="IOutput"/> implementation responsible for rendering the <see cref="OutputBuffer"/> to the console using platform specific methods.
|
||||
/// </summary>
|
||||
public IConsoleOutput Out { get; }
|
||||
public IOutput Output { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class responsible for processing buffered console input and translating
|
||||
/// it into events on the UI thread.
|
||||
/// Gets <see cref="InputProcessor"/> implementation that processes the mouse and keyboard input populated by <see cref="IInput{TInputRecord}"/>
|
||||
/// implementations on the input thread and translating to events on the UI thread.
|
||||
/// </summary>
|
||||
public IInputProcessor InputProcessor { get; }
|
||||
|
||||
@@ -46,24 +46,59 @@ public interface IApplicationMainLoop<T> : IDisposable
|
||||
public AnsiRequestScheduler AnsiRequestScheduler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class responsible for determining the current console size
|
||||
/// Gets the <see cref="ISizeMonitor"/> implementation that tracks terminal size changes.
|
||||
/// </summary>
|
||||
public IConsoleSizeMonitor ConsoleSizeMonitor { get; }
|
||||
public ISizeMonitor SizeMonitor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the loop with a buffer from which data can be read
|
||||
/// Initializes the main loop with its required dependencies.
|
||||
/// </summary>
|
||||
/// <param name="timedEvents"></param>
|
||||
/// <param name="inputBuffer"></param>
|
||||
/// <param name="inputProcessor"></param>
|
||||
/// <param name="consoleOutput"></param>
|
||||
/// <param name="componentFactory"></param>
|
||||
/// <param name="timedEvents">
|
||||
/// The <see cref="ITimedEvents"/> implementation for managing user-defined timeouts and periodic callbacks
|
||||
/// (e.g., <see cref="Application.AddTimeout"/>).
|
||||
/// </param>
|
||||
/// <param name="inputQueue">
|
||||
/// The thread-safe queue containing raw input events populated by <see cref="IInput{TInputRecord}"/> on
|
||||
/// the input thread. This queue is drained by <see cref="InputProcessor"/> during each <see cref="Iteration"/>.
|
||||
/// </param>
|
||||
/// <param name="inputProcessor">
|
||||
/// The <see cref="IInputProcessor"/> that translates raw input records (e.g., <see cref="ConsoleKeyInfo"/>)
|
||||
/// into Terminal.Gui events (<see cref="Key"/>, <see cref="MouseEventArgs"/>) and raises them on the main UI thread.
|
||||
/// </param>
|
||||
/// <param name="output">
|
||||
/// The <see cref="IOutput"/> implementation responsible for rendering the <see cref="OutputBuffer"/> to the
|
||||
/// console using platform-specific methods (e.g., Win32 APIs, ANSI escape sequences).
|
||||
/// </param>
|
||||
/// <param name="componentFactory">
|
||||
/// The factory for creating driver-specific components. Used here to create the <see cref="ISizeMonitor"/>
|
||||
/// that tracks terminal size changes.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called by <see cref="MainLoopCoordinator{TInputRecord}"/> during application startup
|
||||
/// to wire up all the components needed for the main loop to function. It must be called before
|
||||
/// <see cref="Iteration"/> can be invoked.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Initialization order:</b>
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item>Store references to <paramref name="timedEvents"/>, <paramref name="inputQueue"/>,
|
||||
/// <paramref name="inputProcessor"/>, and <paramref name="output"/></item>
|
||||
/// <item>Create <see cref="AnsiRequestScheduler"/> for managing ANSI requests/responses</item>
|
||||
/// <item>Initialize <see cref="OutputBuffer"/> size to match current console dimensions</item>
|
||||
/// <item>Create <see cref="ISizeMonitor"/> using the <paramref name="componentFactory"/></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// After initialization, the main loop is ready to process events via <see cref="Iteration"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void Initialize (
|
||||
ITimedEvents timedEvents,
|
||||
ConcurrentQueue<T> inputBuffer,
|
||||
ConcurrentQueue<TInputRecord> inputQueue,
|
||||
IInputProcessor inputProcessor,
|
||||
IConsoleOutput consoleOutput,
|
||||
IComponentFactory<T> componentFactory
|
||||
IOutput output,
|
||||
IComponentFactory<TInputRecord> componentFactory
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/// <list type="bullet">
|
||||
/// <item>Starting the asynchronous input reading thread</item>
|
||||
/// <item>Initializing the main UI loop on the application thread</item>
|
||||
/// <item>Building the <see cref="IConsoleDriver"/> facade</item>
|
||||
/// <item>Coordinating clean shutdown of both threads</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
@@ -26,7 +25,7 @@ public interface IMainLoopCoordinator
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <returns>A task that completes when initialization is done</returns>
|
||||
public Task StartAsync ();
|
||||
public Task StartInputTaskAsync ();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the input thread and performs cleanup.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Terminal.Gui.Drivers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
@@ -15,50 +13,52 @@ namespace Terminal.Gui.App;
|
||||
/// </para>
|
||||
/// <para>This class is designed to be managed by <see cref="ApplicationImpl"/></para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
|
||||
internal class MainLoopCoordinator<T> : IMainLoopCoordinator
|
||||
/// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
|
||||
internal class MainLoopCoordinator<TInputRecord> : IMainLoopCoordinator where TInputRecord : struct
|
||||
{
|
||||
private readonly ConcurrentQueue<T> _inputBuffer;
|
||||
private readonly IInputProcessor _inputProcessor;
|
||||
private readonly IApplicationMainLoop<T> _loop;
|
||||
private readonly IComponentFactory<T> _componentFactory;
|
||||
private readonly CancellationTokenSource _tokenSource = new ();
|
||||
private IConsoleInput<T> _input;
|
||||
private IConsoleOutput _output;
|
||||
private readonly object _oLockInitialization = new ();
|
||||
private ConsoleDriverFacade<T> _facade;
|
||||
private Task _inputTask;
|
||||
private readonly ITimedEvents _timedEvents;
|
||||
|
||||
private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new coordinator that will manage the main UI loop and input thread.
|
||||
/// </summary>
|
||||
/// <param name="timedEvents">Handles scheduling and execution of user timeout callbacks</param>
|
||||
/// <param name="inputBuffer">Thread-safe queue for buffering raw console input</param>
|
||||
/// <param name="inputQueue">Thread-safe queue for buffering raw console input</param>
|
||||
/// <param name="loop">The main application loop instance</param>
|
||||
/// <param name="componentFactory">Factory for creating driver-specific components (input, output, etc.)</param>
|
||||
public MainLoopCoordinator (
|
||||
ITimedEvents timedEvents,
|
||||
ConcurrentQueue<T> inputBuffer,
|
||||
IApplicationMainLoop<T> loop,
|
||||
IComponentFactory<T> componentFactory
|
||||
ConcurrentQueue<TInputRecord> inputQueue,
|
||||
IApplicationMainLoop<TInputRecord> loop,
|
||||
IComponentFactory<TInputRecord> componentFactory
|
||||
)
|
||||
{
|
||||
_timedEvents = timedEvents;
|
||||
_inputBuffer = inputBuffer;
|
||||
_inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer);
|
||||
_inputQueue = inputQueue;
|
||||
_inputProcessor = componentFactory.CreateInputProcessor (_inputQueue);
|
||||
_loop = loop;
|
||||
_componentFactory = componentFactory;
|
||||
}
|
||||
|
||||
private readonly IApplicationMainLoop<TInputRecord> _loop;
|
||||
private readonly IComponentFactory<TInputRecord> _componentFactory;
|
||||
private readonly CancellationTokenSource _runCancellationTokenSource = new ();
|
||||
private readonly ConcurrentQueue<TInputRecord> _inputQueue;
|
||||
private readonly IInputProcessor _inputProcessor;
|
||||
private readonly object _oLockInitialization = new ();
|
||||
private readonly ITimedEvents _timedEvents;
|
||||
|
||||
private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
|
||||
private IInput<TInputRecord> _input;
|
||||
private Task _inputTask;
|
||||
private IOutput _output;
|
||||
private DriverImpl _driver;
|
||||
|
||||
private bool _stopCalled;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the input loop thread in separate task (returning immediately).
|
||||
/// </summary>
|
||||
public async Task StartAsync ()
|
||||
public async Task StartInputTaskAsync ()
|
||||
{
|
||||
Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
|
||||
Logging.Trace ("Booting... ()");
|
||||
|
||||
_inputTask = Task.Run (RunInput);
|
||||
|
||||
@@ -80,85 +80,21 @@ internal class MainLoopCoordinator<T> : IMainLoopCoordinator
|
||||
throw _inputTask.Exception;
|
||||
}
|
||||
|
||||
Logging.Logger.LogCritical("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
|
||||
Logging.Critical ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
|
||||
}
|
||||
|
||||
Logging.Logger.LogInformation ("Main Loop Coordinator booting complete");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Stop ()
|
||||
{
|
||||
@@ -170,10 +106,91 @@ internal class MainLoopCoordinator<T> : 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread".
|
||||
/// </summary>
|
||||
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<TInputRecord> 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
@@ -55,6 +56,7 @@ internal class MouseImpl : IMouse
|
||||
/// <inheritdoc/>
|
||||
public void RaiseMouseEvent (MouseEventArgs mouseEvent)
|
||||
{
|
||||
//Debug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
|
||||
if (Application?.Initialized is true)
|
||||
{
|
||||
// LastMousePosition is only set if the application is initialized.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>Event arguments for events about <see cref="RunState"/></summary>
|
||||
public class RunStateEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Creates a new instance of the <see cref="RunStateEventArgs"/> class</summary>
|
||||
/// <param name="state"></param>
|
||||
public RunStateEventArgs (RunState state) { State = state; }
|
||||
|
||||
/// <summary>The state being reported on by the event</summary>
|
||||
public RunState State { get; }
|
||||
}
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
|
||||
public class RunState : IDisposable
|
||||
/// <summary>Defines a session token for a running <see cref="Toplevel"/>.</summary>
|
||||
public class SessionToken : IDisposable
|
||||
{
|
||||
/// <summary>Initializes a new <see cref="RunState"/> class.</summary>
|
||||
/// <summary>Initializes a new <see cref="SessionToken"/> class.</summary>
|
||||
/// <param name="view"></param>
|
||||
public RunState (Toplevel view) { Toplevel = view; }
|
||||
public SessionToken (Toplevel view) { Toplevel = view; }
|
||||
|
||||
/// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
|
||||
/// <summary>The <see cref="Toplevel"/> belonging to this <see cref="SessionToken"/>.</summary>
|
||||
public Toplevel Toplevel { get; internal set; }
|
||||
|
||||
/// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
|
||||
/// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>.</remarks>
|
||||
/// <summary>Releases all resource used by the <see cref="SessionToken"/> object.</summary>
|
||||
/// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="SessionToken"/>.</remarks>
|
||||
/// <remarks>
|
||||
/// <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After calling
|
||||
/// <see cref="Dispose()"/>, you must release all references to the <see cref="RunState"/> so the garbage collector can
|
||||
/// reclaim the memory that the <see cref="RunState"/> was occupying.
|
||||
/// <see cref="Dispose()"/> method leaves the <see cref="SessionToken"/> in an unusable state. After calling
|
||||
/// <see cref="Dispose()"/>, you must release all references to the <see cref="SessionToken"/> so the garbage collector can
|
||||
/// reclaim the memory that the <see cref="SessionToken"/> was occupying.
|
||||
/// </remarks>
|
||||
public void Dispose ()
|
||||
{
|
||||
@@ -28,7 +28,7 @@ public class RunState : IDisposable
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
|
||||
/// <summary>Releases all resource used by the <see cref="SessionToken"/> object.</summary>
|
||||
/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
|
||||
protected virtual void Dispose (bool disposing)
|
||||
{
|
||||
@@ -38,7 +38,7 @@ public class RunState : IDisposable
|
||||
// But that is not correct becaue `Begin` didn't create the TopLevel, `Init` did; thus
|
||||
// disposing should be done by `Shutdown`, not `End`.
|
||||
throw new InvalidOperationException (
|
||||
"Toplevel must be null before calling Application.RunState.Dispose"
|
||||
"Toplevel must be null before calling Application.SessionToken.Dispose"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,29 +46,29 @@ public class RunState : IDisposable
|
||||
#if DEBUG_IDISPOSABLE
|
||||
#pragma warning disable CS0419 // Ambiguous reference in cref attribute
|
||||
/// <summary>
|
||||
/// Gets whether <see cref="RunState.Dispose"/> was called on this RunState or not.
|
||||
/// Gets whether <see cref="SessionToken.Dispose"/> was called on this SessionToken or not.
|
||||
/// For debug purposes to verify objects are being disposed properly.
|
||||
/// Only valid when DEBUG_IDISPOSABLE is defined.
|
||||
/// </summary>
|
||||
public bool WasDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times <see cref="RunState.Dispose"/> was called on this object.
|
||||
/// Gets the number of times <see cref="SessionToken.Dispose"/> was called on this object.
|
||||
/// For debug purposes to verify objects are being disposed properly.
|
||||
/// Only valid when DEBUG_IDISPOSABLE is defined.
|
||||
/// </summary>
|
||||
public int DisposedCount { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of RunState objects that have been created and not yet disposed.
|
||||
/// Note, this is a static property and will affect all RunState objects.
|
||||
/// Gets the list of SessionToken objects that have been created and not yet disposed.
|
||||
/// Note, this is a static property and will affect all SessionToken objects.
|
||||
/// For debug purposes to verify objects are being disposed properly.
|
||||
/// Only valid when DEBUG_IDISPOSABLE is defined.
|
||||
/// </summary>
|
||||
public static ConcurrentBag<RunState> Instances { get; private set; } = [];
|
||||
public static ConcurrentBag<SessionToken> Instances { get; private set; } = [];
|
||||
|
||||
/// <summary>Creates a new RunState object.</summary>
|
||||
public RunState ()
|
||||
/// <summary>Creates a new SessionToken object.</summary>
|
||||
public SessionToken ()
|
||||
{
|
||||
Instances.Add (this);
|
||||
}
|
||||
12
Terminal.Gui/App/SessionTokenEventArgs.cs
Normal file
12
Terminal.Gui/App/SessionTokenEventArgs.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Terminal.Gui.App;
|
||||
|
||||
/// <summary>Event arguments for events about <see cref="SessionToken"/></summary>
|
||||
public class SessionTokenEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Creates a new instance of the <see cref="SessionTokenEventArgs"/> class</summary>
|
||||
/// <param name="state"></param>
|
||||
public SessionTokenEventArgs (SessionToken state) { State = state; }
|
||||
|
||||
/// <summary>The state being reported on by the event</summary>
|
||||
public SessionToken State { get; }
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public interface ITimedEvents
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new timeout is added. To be used in the case when
|
||||
/// <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
|
||||
/// <see cref="IApplication.StopAfterFirstIteration"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
event EventHandler<TimeoutEventArgs>? Added;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Terminal.Gui.Drawing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
|
||||
/// <see cref="IConsoleDriver"/>).
|
||||
/// <see cref="IDriver"/>).
|
||||
/// </summary>
|
||||
public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default)
|
||||
{
|
||||
|
||||
@@ -402,7 +402,7 @@ public class LineCanvas : IDisposable
|
||||
// TODO: Add other resolvers
|
||||
};
|
||||
|
||||
private Cell? GetCellForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
private Cell? GetCellForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
{
|
||||
if (intersects.IsEmpty)
|
||||
{
|
||||
@@ -422,7 +422,7 @@ public class LineCanvas : IDisposable
|
||||
return cell;
|
||||
}
|
||||
|
||||
private Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
private Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
{
|
||||
if (intersects.IsEmpty)
|
||||
{
|
||||
@@ -769,7 +769,7 @@ public class LineCanvas : IDisposable
|
||||
internal Rune _thickV;
|
||||
protected IntersectionRuneResolver () { SetGlyphs (); }
|
||||
|
||||
public Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
public Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
|
||||
{
|
||||
// Note that there aren't any glyphs for intersections of double lines with heavy lines
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ internal class Ruler
|
||||
/// <param name="location">The location to start drawing the ruler, in screen-relative coordinates.</param>
|
||||
/// <param name="start">The start value of the ruler.</param>
|
||||
/// <param name="driver">Optional Driver. If not provided, driver will be used.</param>
|
||||
public void Draw (Point location, int start = 0, IConsoleDriver? driver = null)
|
||||
public void Draw (Point location, int start = 0, IDriver? driver = null)
|
||||
{
|
||||
if (start < 0)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/// <summary>
|
||||
/// Describes a request to render a given <see cref="SixelData"/> at a given <see cref="ScreenPosition"/>.
|
||||
/// Requires that the terminal and <see cref="IConsoleDriver"/> both support sixel.
|
||||
/// Requires that the terminal and <see cref="IDriver"/> both support sixel.
|
||||
/// </summary>
|
||||
public class SixelToRender
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ public record struct Thickness
|
||||
/// <param name="label">The diagnostics label to draw on the bottom of the <see cref="Bottom"/>.</param>
|
||||
/// <param name="driver">Optional driver. If not specified, <see cref="Application.Driver"/> will be used.</param>
|
||||
/// <returns>The inner rectangle remaining to be drawn.</returns>
|
||||
public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null, IConsoleDriver? driver = null)
|
||||
public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null, IDriver? driver = null)
|
||||
{
|
||||
if (rect.Size.Width < 1 || rect.Size.Height < 1)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends the <see cref="AnsiEscapeSequence.Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
|
||||
/// Sends the <see cref="AnsiEscapeSequence.Request"/> to the raw output stream of the current <see cref="IDriver"/>.
|
||||
/// Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
|
||||
/// sending many requests.
|
||||
/// </summary>
|
||||
|
||||
@@ -141,6 +141,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
|
||||
bool isEscape = currentChar == ESCAPE;
|
||||
|
||||
// Logging.Trace($"Processing character '{currentChar}' (isEscape: {isEscape})");
|
||||
switch (State)
|
||||
{
|
||||
case AnsiResponseParserState.Normal:
|
||||
@@ -261,7 +262,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
{
|
||||
_heldContent.ClearHeld ();
|
||||
|
||||
Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
|
||||
//Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,7 +343,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
{
|
||||
_heldContent.ClearHeld ();
|
||||
|
||||
Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
|
||||
//Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
|
||||
|
||||
// Do not send back to input stream
|
||||
return false;
|
||||
@@ -403,7 +404,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
|
||||
if (matchingResponse?.Response != null)
|
||||
{
|
||||
Logging.Trace ($"AnsiResponseParser processed '{cur}'");
|
||||
//Logging.Trace ($"AnsiResponseParser processed '{cur}'");
|
||||
|
||||
if (invokeCallback)
|
||||
{
|
||||
@@ -479,16 +480,14 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
}
|
||||
}
|
||||
|
||||
internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
||||
internal class AnsiResponseParser<TInputRecord> () : AnsiResponseParserBase (new GenericHeld<TInputRecord> ())
|
||||
{
|
||||
public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
|
||||
|
||||
/// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
|
||||
public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
|
||||
public Func<IEnumerable<Tuple<char, TInputRecord>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
|
||||
|
||||
public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
|
||||
public IEnumerable<Tuple<char, TInputRecord>> ProcessInput (params Tuple<char, TInputRecord> [] input)
|
||||
{
|
||||
List<Tuple<char, T>> output = new ();
|
||||
List<Tuple<char, TInputRecord>> output = [];
|
||||
|
||||
ProcessInputBase (
|
||||
i => input [i].Item1,
|
||||
@@ -499,22 +498,22 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
||||
return output;
|
||||
}
|
||||
|
||||
private void AppendOutput (List<Tuple<char, T>> output, object c)
|
||||
private void AppendOutput (List<Tuple<char, TInputRecord>> output, object c)
|
||||
{
|
||||
Tuple<char, T> tuple = (Tuple<char, T>)c;
|
||||
Tuple<char, TInputRecord> tuple = (Tuple<char, TInputRecord>)c;
|
||||
|
||||
Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
|
||||
//Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
|
||||
output.Add (tuple);
|
||||
}
|
||||
|
||||
public Tuple<char, T> [] Release ()
|
||||
public Tuple<char, TInputRecord> [] Release ()
|
||||
{
|
||||
// Lock in case Release is called from different Thread from parse
|
||||
lock (_lockState)
|
||||
{
|
||||
TryLastMinuteSequences ();
|
||||
|
||||
Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
|
||||
Tuple<char, TInputRecord> [] result = HeldToEnumerable ().ToArray ();
|
||||
|
||||
ResetState ();
|
||||
|
||||
@@ -522,7 +521,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Tuple<char, T>> HeldToEnumerable () { return (IEnumerable<Tuple<char, T>>)_heldContent.HeldToObjects (); }
|
||||
private IEnumerable<Tuple<char, TInputRecord>> HeldToEnumerable () { return (IEnumerable<Tuple<char, TInputRecord>>)_heldContent.HeldToObjects (); }
|
||||
|
||||
/// <summary>
|
||||
/// 'Overload' for specifying an expectation that requires the metadata as well as characters. Has
|
||||
@@ -532,7 +531,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
||||
/// <param name="response"></param>
|
||||
/// <param name="abandoned"></param>
|
||||
/// <param name="persistent"></param>
|
||||
public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
|
||||
public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, TInputRecord>>> response, Action? abandoned, bool persistent)
|
||||
{
|
||||
lock (_lockExpectedResponses)
|
||||
{
|
||||
@@ -581,7 +580,7 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
|
||||
|
||||
private void AppendOutput (StringBuilder output, char c)
|
||||
{
|
||||
Logging.Trace ($"AnsiResponseParser releasing '{c}'");
|
||||
// Logging.Trace ($"AnsiResponseParser releasing '{c}'");
|
||||
output.Append (c);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{T}"/>
|
||||
/// Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{TInputRecord}"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal class GenericHeld<T> : IHeld
|
||||
/// <typeparam name="TInputRecord"></typeparam>
|
||||
internal class GenericHeld<TInputRecord> : IHeld
|
||||
{
|
||||
private readonly List<Tuple<char, T>> held = new ();
|
||||
private readonly List<Tuple<char, TInputRecord>> held = [];
|
||||
|
||||
public void ClearHeld () { held.Clear (); }
|
||||
|
||||
@@ -15,7 +15,7 @@ internal class GenericHeld<T> : IHeld
|
||||
|
||||
public IEnumerable<object> HeldToObjects () { return held; }
|
||||
|
||||
public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
|
||||
public void AddToHeld (object o) { held.Add ((Tuple<char, TInputRecord>)o); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length => held.Count;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class implementation of <see cref="IComponentFactory{T}"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class ComponentFactory<T> : IComponentFactory<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract IConsoleInput<T> CreateInput ();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue<T> inputBuffer);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
|
||||
{
|
||||
return new ConsoleSizeMonitor (consoleOutput, outputBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IConsoleOutput CreateOutput ();
|
||||
}
|
||||
25
Terminal.Gui/Drivers/ComponentFactoryImpl.cs
Normal file
25
Terminal.Gui/Drivers/ComponentFactoryImpl.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class implementation of <see cref="IComponentFactory{TInputRecord}"/> that provides a default implementation of <see cref="CreateSizeMonitor"/>.</summary>
|
||||
/// <typeparam name="TInputRecord">The platform specific keyboard input type (e.g. <see cref="ConsoleKeyInfo"/> or <see cref="WindowsConsole.InputRecord"/></typeparam>
|
||||
public abstract class ComponentFactoryImpl<TInputRecord> : IComponentFactory<TInputRecord> where TInputRecord : struct
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract IInput<TInputRecord> CreateInput ();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue<TInputRecord> inputBuffer);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
|
||||
{
|
||||
return new SizeMonitorImpl (consoleOutput);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IOutput CreateOutput ();
|
||||
}
|
||||
@@ -1,753 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>Base class for Terminal.Gui IConsoleDriver implementations.</summary>
|
||||
/// <remarks>
|
||||
/// There are currently four implementations:
|
||||
/// - DotNetDriver that uses the .NET Console API and works on all platforms
|
||||
/// - UnixDriver optimized for Unix and Mac.
|
||||
/// - WindowsDriver optimized for Windows.
|
||||
/// - FakeDriver for unit testing.
|
||||
/// </remarks>
|
||||
public abstract class ConsoleDriver : IConsoleDriver
|
||||
{
|
||||
/// <summary>
|
||||
/// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
|
||||
/// <code>
|
||||
/// public ColorTests ()
|
||||
/// {
|
||||
/// ConsoleDriver.RunningUnitTests = true;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
internal static bool RunningUnitTests { get; set; }
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
public IClipboard? Clipboard { get; internal set; }
|
||||
|
||||
/// <summary>Returns the name of the driver and relevant library version information.</summary>
|
||||
/// <returns></returns>
|
||||
public virtual string GetVersionInfo () { return GetType ().Name; }
|
||||
|
||||
#region ANSI Esc Sequence Handling
|
||||
|
||||
// QUESTION: This appears to be an API to help in debugging. It's only implemented in UnixDriver and WindowsDriver.
|
||||
// QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
|
||||
/// <summary>
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="ansi"></param>
|
||||
public abstract void WriteRaw (string ansi);
|
||||
|
||||
#endregion ANSI Esc Sequence Handling
|
||||
|
||||
#region Screen and Contents
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
|
||||
/// </summary>
|
||||
public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50);
|
||||
|
||||
// As performance is a concern, we keep track of the dirty lines and only refresh those.
|
||||
// This is in addition to the dirty flag on each cell.
|
||||
internal bool []? _dirtyLines;
|
||||
|
||||
// QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application?
|
||||
/// <summary>Gets the location and size of the terminal screen.</summary>
|
||||
public Rectangle Screen => new (0, 0, Cols, Rows);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the screen size for testing purposes. Only supported by FakeDriver.
|
||||
/// <see cref="Screen"/> is the source of truth for screen dimensions.
|
||||
/// <see cref="Cols"/> and <see cref="Rows"/> are read-only and derived from <see cref="Screen"/>.
|
||||
/// </summary>
|
||||
/// <param name="width">The new width in columns.</param>
|
||||
/// <param name="height">The new height in rows.</param>
|
||||
/// <exception cref="NotSupportedException">Thrown when called on non-FakeDriver instances.</exception>
|
||||
public virtual void SetScreenSize (int width, int height)
|
||||
{
|
||||
throw new NotSupportedException ("SetScreenSize is only supported by FakeDriver for test scenarios.");
|
||||
}
|
||||
|
||||
private Region? _clip;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
|
||||
/// to.
|
||||
/// </summary>
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Region? Clip
|
||||
{
|
||||
get => _clip;
|
||||
set
|
||||
{
|
||||
if (_clip == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_clip = value;
|
||||
|
||||
// Don't ever let Clip be bigger than Screen
|
||||
if (_clip is { })
|
||||
{
|
||||
_clip.Intersect (Screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Col { get; private set; }
|
||||
|
||||
/// <summary>The number of columns visible in the terminal.</summary>
|
||||
public virtual int Cols
|
||||
{
|
||||
get => _cols;
|
||||
set
|
||||
{
|
||||
_cols = value;
|
||||
ClearContents ();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The contents of the application output. The driver outputs this buffer to the terminal when
|
||||
/// <see cref="UpdateScreen"/> is called.
|
||||
/// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
|
||||
/// </summary>
|
||||
public Cell [,]? Contents { get; set; }
|
||||
|
||||
/// <summary>The leftmost column in the terminal.</summary>
|
||||
public virtual int Left { get; set; } = 0;
|
||||
|
||||
/// <summary>Tests if the specified rune is supported by the driver.</summary>
|
||||
/// <param name="rune"></param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
|
||||
/// support displaying this rune.
|
||||
/// </returns>
|
||||
public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
|
||||
|
||||
/// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
||||
/// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
|
||||
/// <para>
|
||||
/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="Cols"/> and
|
||||
/// <see cref="Rows"/>, the method still sets those properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
public virtual void Move (int col, int row)
|
||||
{
|
||||
//Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
|
||||
Col = col;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Row { get; private set; }
|
||||
|
||||
/// <summary>The number of rows visible in the terminal.</summary>
|
||||
public virtual int Rows
|
||||
{
|
||||
get => _rows;
|
||||
set
|
||||
{
|
||||
_rows = value;
|
||||
ClearContents ();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The topmost row in the terminal.</summary>
|
||||
public virtual int Top { get; set; } = 0;
|
||||
|
||||
/// <summary>Adds the specified rune to the display at the current cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
|
||||
/// dimensions defined by <see cref="Cols"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns
|
||||
/// needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
|
||||
/// will be added instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="rune">Rune to add.</param>
|
||||
public void AddRune (Rune rune)
|
||||
{
|
||||
int runeWidth = -1;
|
||||
bool validLocation = IsValidLocation (rune, Col, Row);
|
||||
|
||||
if (Contents is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle clipRect = Clip!.GetBounds ();
|
||||
|
||||
if (validLocation)
|
||||
{
|
||||
rune = rune.MakePrintable ();
|
||||
runeWidth = rune.GetColumns ();
|
||||
|
||||
lock (Contents)
|
||||
{
|
||||
if (runeWidth == 0 && rune.IsCombiningMark ())
|
||||
{
|
||||
// AtlasEngine does not support NON-NORMALIZED combining marks in a way
|
||||
// compatible with the driver architecture. Any CMs (except in the first col)
|
||||
// are correctly combined with the base char, but are ALSO treated as 1 column
|
||||
// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
|
||||
//
|
||||
// Until this is addressed (see Issue #), we do our best by
|
||||
// a) Attempting to normalize any CM with the base char to it's left
|
||||
// b) Ignoring any CMs that don't normalize
|
||||
if (Col > 0)
|
||||
{
|
||||
if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
|
||||
{
|
||||
// Just add this mark to the list
|
||||
Contents [Row, Col - 1].AddCombiningMark (rune);
|
||||
|
||||
// Ignore. Don't move to next column (let the driver figure out what to do).
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to normalize the cell to our left combined with this mark
|
||||
string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
|
||||
|
||||
// Normalize to Form C (Canonical Composition)
|
||||
string normalized = combined.Normalize (NormalizationForm.FormC);
|
||||
|
||||
if (normalized.Length == 1)
|
||||
{
|
||||
// It normalized! We can just set the Cell to the left with the
|
||||
// normalized codepoint
|
||||
Contents [Row, Col - 1].Rune = (Rune)normalized [0];
|
||||
|
||||
// Ignore. Don't move to next column because we're already there
|
||||
}
|
||||
else
|
||||
{
|
||||
// It didn't normalize. Add it to the Cell to left's CM list
|
||||
Contents [Row, Col - 1].AddCombiningMark (rune);
|
||||
|
||||
// Ignore. Don't move to next column (let the driver figure out what to do).
|
||||
}
|
||||
}
|
||||
|
||||
Contents [Row, Col - 1].Attribute = CurrentAttribute;
|
||||
Contents [Row, Col - 1].IsDirty = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Most drivers will render a combining mark at col 0 as the mark
|
||||
Contents [Row, Col].Rune = rune;
|
||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
||||
Contents [Row, Col].IsDirty = true;
|
||||
Col++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
||||
Contents [Row, Col].IsDirty = true;
|
||||
|
||||
if (Col > 0)
|
||||
{
|
||||
// Check if cell to left has a wide glyph
|
||||
if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
|
||||
{
|
||||
// Invalidate cell to left
|
||||
Contents [Row, Col - 1].Rune = (Rune)'\0';
|
||||
Contents [Row, Col - 1].IsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (runeWidth < 1)
|
||||
{
|
||||
Contents [Row, Col].Rune = Rune.ReplacementChar;
|
||||
}
|
||||
else if (runeWidth == 1)
|
||||
{
|
||||
Contents [Row, Col].Rune = rune;
|
||||
|
||||
if (Col < clipRect.Right - 1)
|
||||
{
|
||||
Contents [Row, Col + 1].IsDirty = true;
|
||||
}
|
||||
}
|
||||
else if (runeWidth == 2)
|
||||
{
|
||||
if (!Clip.Contains (Col + 1, Row))
|
||||
{
|
||||
// We're at the right edge of the clip, so we can't display a wide character.
|
||||
// TODO: Figure out if it is better to show a replacement character or ' '
|
||||
Contents [Row, Col].Rune = Rune.ReplacementChar;
|
||||
}
|
||||
else if (!Clip.Contains (Col, Row))
|
||||
{
|
||||
// Our 1st column is outside the clip, so we can't display a wide character.
|
||||
Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
|
||||
}
|
||||
else
|
||||
{
|
||||
Contents [Row, Col].Rune = rune;
|
||||
|
||||
if (Col < clipRect.Right - 1)
|
||||
{
|
||||
// Invalidate cell to right so that it doesn't get drawn
|
||||
// TODO: Figure out if it is better to show a replacement character or ' '
|
||||
Contents [Row, Col + 1].Rune = (Rune)'\0';
|
||||
Contents [Row, Col + 1].IsDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a non-spacing character, so we don't need to do anything
|
||||
Contents [Row, Col].Rune = (Rune)' ';
|
||||
Contents [Row, Col].IsDirty = false;
|
||||
}
|
||||
|
||||
_dirtyLines! [Row] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (runeWidth is < 0 or > 0)
|
||||
{
|
||||
Col++;
|
||||
}
|
||||
|
||||
if (runeWidth > 1)
|
||||
{
|
||||
Debug.Assert (runeWidth <= 2);
|
||||
|
||||
if (validLocation && Col < clipRect.Right)
|
||||
{
|
||||
lock (Contents!)
|
||||
{
|
||||
// This is a double-width character, and we are not at the end of the line.
|
||||
// Col now points to the second column of the character. Ensure it doesn't
|
||||
// Get rendered.
|
||||
Contents [Row, Col].IsDirty = false;
|
||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
||||
|
||||
// TODO: Determine if we should wipe this out (for now now)
|
||||
//Contents [Row, Col].Rune = (Rune)' ';
|
||||
}
|
||||
}
|
||||
|
||||
Col++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
|
||||
/// convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
|
||||
/// </summary>
|
||||
/// <param name="c">Character to add.</param>
|
||||
public void AddRune (char c) { AddRune (new Rune (c)); }
|
||||
|
||||
/// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen
|
||||
/// dimensions defined by <see cref="Cols"/>.
|
||||
/// </para>
|
||||
/// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
|
||||
/// </remarks>
|
||||
/// <param name="str">String.</param>
|
||||
public void AddStr (string str)
|
||||
{
|
||||
List<Rune> runes = str.EnumerateRunes ().ToList ();
|
||||
|
||||
for (var i = 0; i < runes.Count; i++)
|
||||
{
|
||||
AddRune (runes [i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
|
||||
/// <remarks>
|
||||
/// The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
|
||||
/// </remarks>
|
||||
/// <param name="rect">The Screen-relative rectangle.</param>
|
||||
/// <param name="rune">The Rune used to fill the rectangle</param>
|
||||
public void FillRect (Rectangle rect, Rune rune = default)
|
||||
{
|
||||
// BUGBUG: This should be a method on Region
|
||||
rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
|
||||
lock (Contents!)
|
||||
{
|
||||
for (int r = rect.Y; r < rect.Y + rect.Height; r++)
|
||||
{
|
||||
for (int c = rect.X; c < rect.X + rect.Width; c++)
|
||||
{
|
||||
if (!IsValidLocation (rune, c, r))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Contents [r, c] = new Cell
|
||||
{
|
||||
Rune = rune != default ? rune : (Rune)' ',
|
||||
Attribute = CurrentAttribute, IsDirty = true
|
||||
};
|
||||
_dirtyLines! [r] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
|
||||
public void ClearContents ()
|
||||
{
|
||||
Contents = new Cell [Rows, Cols];
|
||||
|
||||
//CONCURRENCY: Unsynchronized access to Clip isn't safe.
|
||||
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
|
||||
Clip = new (Screen);
|
||||
|
||||
_dirtyLines = new bool [Rows];
|
||||
|
||||
lock (Contents)
|
||||
{
|
||||
for (var row = 0; row < Rows; row++)
|
||||
{
|
||||
for (var c = 0; c < Cols; c++)
|
||||
{
|
||||
Contents [row, c] = new ()
|
||||
{
|
||||
Rune = (Rune)' ',
|
||||
Attribute = new Attribute (Color.White, Color.Black),
|
||||
IsDirty = true
|
||||
};
|
||||
}
|
||||
|
||||
_dirtyLines [row] = true;
|
||||
}
|
||||
}
|
||||
|
||||
ClearedContents?.Invoke (this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised each time <see cref="ClearContents"/> is called. For benchmarking.
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs>? ClearedContents;
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="Contents"/> as dirty for situations where views
|
||||
/// don't need layout and redrawing, but just refresh the screen.
|
||||
/// </summary>
|
||||
protected void SetContentsAsDirty ()
|
||||
{
|
||||
lock (Contents!)
|
||||
{
|
||||
for (var row = 0; row < Rows; row++)
|
||||
{
|
||||
for (var c = 0; c < Cols; c++)
|
||||
{
|
||||
Contents [row, c].IsDirty = true;
|
||||
}
|
||||
|
||||
_dirtyLines! [row] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="FillRect(Rectangle, Rune)"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="c"></param>
|
||||
public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
|
||||
|
||||
#endregion Screen and Contents
|
||||
|
||||
#region Cursor Handling
|
||||
|
||||
/// <summary>Gets the terminal cursor visibility.</summary>
|
||||
/// <param name="visibility">The current <see cref="CursorVisibility"/></param>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool GetCursorVisibility (out CursorVisibility visibility);
|
||||
|
||||
/// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary>
|
||||
/// <param name="rune">Used to determine if one or two columns are required.</param>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
public bool IsValidLocation (Rune rune, int col, int row)
|
||||
{
|
||||
if (rune.GetColumns () < 2)
|
||||
{
|
||||
return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the terminal screen changes (size, position, etc.). Fires the <see cref="SizeChanged"/> event.
|
||||
/// <see cref="Screen"/> reflects the source of truth for screen dimensions.
|
||||
/// <see cref="Cols"/> and <see cref="Rows"/> are derived from <see cref="Screen"/> and are read-only.
|
||||
/// </summary>
|
||||
/// <param name="args">Event arguments containing the new screen size.</param>
|
||||
public void OnSizeChanged (SizeChangedEventArgs args)
|
||||
{
|
||||
SizeChanged?.Invoke (this, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
|
||||
public void Refresh ()
|
||||
{
|
||||
bool updated = UpdateScreen ();
|
||||
UpdateCursor ();
|
||||
|
||||
Refreshed?.Invoke (this, new EventArgs<bool> (in updated));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised each time <see cref="Refresh"/> is called. For benchmarking.
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs<bool>>? Refreshed;
|
||||
|
||||
/// <summary>Sets the terminal cursor visibility.</summary>
|
||||
/// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool SetCursorVisibility (CursorVisibility visibility);
|
||||
|
||||
/// <summary>
|
||||
/// The event fired when the screen changes (size, position, etc.).
|
||||
/// <see cref="Screen"/> is the source of truth for screen dimensions.
|
||||
/// <see cref="Cols"/> and <see cref="Rows"/> are read-only and derived from <see cref="Screen"/>.
|
||||
/// </summary>
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
|
||||
#endregion Cursor Handling
|
||||
|
||||
/// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
|
||||
/// <remarks>This is only implemented in the Unix driver.</remarks>
|
||||
public abstract void Suspend ();
|
||||
|
||||
/// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
|
||||
public abstract void UpdateCursor ();
|
||||
|
||||
/// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
|
||||
/// <returns><see langword="true"/> if any updates to the screen were made.</returns>
|
||||
public abstract bool UpdateScreen ();
|
||||
|
||||
#region Setup & Teardown
|
||||
|
||||
/// <summary>Initializes the driver</summary>
|
||||
public abstract void Init ();
|
||||
|
||||
/// <summary>Ends the execution of the console driver.</summary>
|
||||
public abstract void End ();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Color Handling
|
||||
|
||||
/// <summary>Gets whether the <see cref="IConsoleDriver"/> supports TrueColor output.</summary>
|
||||
public virtual bool SupportsTrueColor => true;
|
||||
|
||||
// TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
|
||||
// BUGBUG: Application.Force16Colors should be bool? so if SupportsTrueColor and Application.Force16Colors == false, this doesn't override
|
||||
/// <summary>
|
||||
/// Gets or sets whether the <see cref="IConsoleDriver"/> should use 16 colors instead of the default TrueColors.
|
||||
/// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Will be forced to <see langword="true"/> if <see cref="IConsoleDriver.SupportsTrueColor"/> is
|
||||
/// <see langword="false"/>, indicating that the <see cref="IConsoleDriver"/> cannot support TrueColor.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual bool Force16Colors
|
||||
{
|
||||
get => Application.Force16Colors || !SupportsTrueColor;
|
||||
set => Application.Force16Colors = value || !SupportsTrueColor;
|
||||
}
|
||||
|
||||
private int _cols;
|
||||
private int _rows;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
|
||||
/// call.
|
||||
/// </summary>
|
||||
public Attribute CurrentAttribute { get; set; }
|
||||
|
||||
/// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
|
||||
/// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
|
||||
/// <param name="c">C.</param>
|
||||
public Attribute SetAttribute (Attribute c)
|
||||
{
|
||||
Attribute prevAttribute = CurrentAttribute;
|
||||
CurrentAttribute = c;
|
||||
|
||||
return prevAttribute;
|
||||
}
|
||||
|
||||
/// <summary>Gets the current <see cref="Attribute"/>.</summary>
|
||||
/// <returns>The current attribute.</returns>
|
||||
public Attribute GetAttribute () { return CurrentAttribute; }
|
||||
|
||||
#endregion Color Handling
|
||||
|
||||
#region Mouse Handling
|
||||
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnMouseEvent (MouseEventArgs a)
|
||||
{
|
||||
// Ensure ScreenPosition is set
|
||||
a.ScreenPosition = a.Position;
|
||||
|
||||
MouseEvent?.Invoke (this, a);
|
||||
}
|
||||
|
||||
#endregion Mouse Handling
|
||||
|
||||
#region Keyboard Handling
|
||||
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
|
||||
public event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
|
||||
/// <see cref="OnKeyUp"/>.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); }
|
||||
|
||||
/// <summary>Event fired when a key is released.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
public event EventHandler<Key>? KeyUp;
|
||||
|
||||
/// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
|
||||
/// is complete.
|
||||
/// </remarks>
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
|
||||
|
||||
internal char _highSurrogate = '\0';
|
||||
|
||||
internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
|
||||
{
|
||||
result = keyCode;
|
||||
|
||||
if (char.IsHighSurrogate ((char)keyCode))
|
||||
{
|
||||
_highSurrogate = (char)keyCode;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
|
||||
{
|
||||
result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
|
||||
|
||||
if ((keyCode & KeyCode.AltMask) != 0)
|
||||
{
|
||||
result |= KeyCode.AltMask;
|
||||
}
|
||||
|
||||
if ((keyCode & KeyCode.CtrlMask) != 0)
|
||||
{
|
||||
result |= KeyCode.CtrlMask;
|
||||
}
|
||||
|
||||
if ((keyCode & KeyCode.ShiftMask) != 0)
|
||||
{
|
||||
result |= KeyCode.ShiftMask;
|
||||
}
|
||||
|
||||
_highSurrogate = '\0';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (char.IsSurrogate ((char)keyCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_highSurrogate > 0)
|
||||
{
|
||||
_highSurrogate = '\0';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private AnsiRequestScheduler? _scheduler;
|
||||
|
||||
/// <summary>
|
||||
/// Queues the given <paramref name="request"/> for execution
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
public void QueueAnsiRequest (AnsiEscapeSequenceRequest request)
|
||||
{
|
||||
GetRequestScheduler ().SendOrSchedule (request);
|
||||
}
|
||||
|
||||
internal abstract IAnsiResponseParser GetParser ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AnsiRequestScheduler"/> for this <see cref="ConsoleDriver"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public AnsiRequestScheduler GetRequestScheduler ()
|
||||
{
|
||||
// Lazy initialization because GetParser is virtual
|
||||
return _scheduler ??= new (GetParser ());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for reading console input in perpetual loop
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class ConsoleInput<T> : IConsoleInput<T>
|
||||
{
|
||||
private ConcurrentQueue<T>? _inputBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how to get the current system type, adjust
|
||||
/// in unit tests to simulate specific timings.
|
||||
/// </summary>
|
||||
public Func<DateTime> Now { get; set; } = () => DateTime.Now;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Dispose () { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize (ConcurrentQueue<T> inputBuffer) { _inputBuffer = inputBuffer; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Run (CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_inputBuffer == null)
|
||||
{
|
||||
throw new ("Cannot run input before Initialization");
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
DateTime dt = Now ();
|
||||
|
||||
while (Peek ())
|
||||
{
|
||||
foreach (T r in Read ())
|
||||
{
|
||||
_inputBuffer.Enqueue (r);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSpan took = Now () - dt;
|
||||
TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took;
|
||||
|
||||
Logging.DrainInputStream.Record (took.Milliseconds);
|
||||
|
||||
if (sleepFor.Milliseconds > 0)
|
||||
{
|
||||
Task.Delay (sleepFor, token).Wait (token);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested ();
|
||||
}
|
||||
while (!token.IsCancellationRequested);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When implemented in a derived class, returns true if there is data available
|
||||
/// to read from console.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract bool Peek ();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the available data without blocking, called when <see cref="Peek"/>
|
||||
/// returns <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract IEnumerable<T> Read ();
|
||||
}
|
||||
95
Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs
Normal file
95
Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ConsoleKeyInfo"/>.
|
||||
/// </summary>
|
||||
public static class ConsoleKeyInfoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a string representation of the <see cref="ConsoleKeyInfo"/> suitable for debugging and logging.
|
||||
/// </summary>
|
||||
/// <param name="consoleKeyInfo">The ConsoleKeyInfo to convert to string.</param>
|
||||
/// <returns>A formatted string showing the key, character, and modifiers.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Examples:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>Key: A ('a')</c> - lowercase 'a' pressed</item>
|
||||
/// <item><c>Key: A ('A'), Modifiers: Shift</c> - uppercase 'A' pressed</item>
|
||||
/// <item><c>Key: A (\0), Modifiers: Control</c> - Ctrl+A (no printable char)</item>
|
||||
/// <item><c>Key: Enter (0x000D)</c> - Enter key (carriage return)</item>
|
||||
/// <item><c>Key: F5 (\0)</c> - F5 function key</item>
|
||||
/// <item><c>Key: D2 ('@'), Modifiers: Shift</c> - Shift+2 on US keyboard</item>
|
||||
/// <item><c>Key: None ('<27>')</c> - Accented character</item>
|
||||
/// <item><c>Key: CursorUp (\0), Modifiers: Shift | Control</c> - Ctrl+Shift+Up Arrow</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static string ToString (this ConsoleKeyInfo consoleKeyInfo)
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
|
||||
// Always show the ConsoleKey enum value
|
||||
sb.Append ("Key: ");
|
||||
sb.Append (consoleKeyInfo.Key);
|
||||
|
||||
// Show the character if it's printable, otherwise show hex representation
|
||||
sb.Append (" (");
|
||||
|
||||
if (consoleKeyInfo.KeyChar >= 32 && consoleKeyInfo.KeyChar <= 126) // Printable ASCII range
|
||||
{
|
||||
sb.Append ('\'');
|
||||
sb.Append (consoleKeyInfo.KeyChar);
|
||||
sb.Append ('\'');
|
||||
}
|
||||
else if (consoleKeyInfo.KeyChar == 0)
|
||||
{
|
||||
sb.Append ("\\0");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show special characters or non-printable as hex
|
||||
sb.Append ("0x");
|
||||
sb.Append (((int)consoleKeyInfo.KeyChar).ToString ("X4"));
|
||||
}
|
||||
|
||||
sb.Append (')');
|
||||
|
||||
// Show modifiers if any are set
|
||||
if (consoleKeyInfo.Modifiers != 0)
|
||||
{
|
||||
sb.Append (", Modifiers: ");
|
||||
|
||||
var needsSeparator = false;
|
||||
|
||||
if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0)
|
||||
{
|
||||
sb.Append ("Shift");
|
||||
needsSeparator = true;
|
||||
}
|
||||
|
||||
if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0)
|
||||
{
|
||||
if (needsSeparator)
|
||||
{
|
||||
sb.Append (" | ");
|
||||
}
|
||||
|
||||
sb.Append ("Alt");
|
||||
needsSeparator = true;
|
||||
}
|
||||
|
||||
if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0)
|
||||
{
|
||||
if (needsSeparator)
|
||||
{
|
||||
sb.Append (" | ");
|
||||
}
|
||||
|
||||
sb.Append ("Control");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
|
||||
/// <summary>
|
||||
/// Wraps IConsoleInput for .NET console input events (ConsoleKeyInfo). Needed to support Mocking in tests.
|
||||
/// </summary>
|
||||
internal interface INetInput : IInput<ConsoleKeyInfo>
|
||||
{ }
|
||||
|
||||
@@ -4,26 +4,17 @@ using System.Collections.Concurrent;
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IComponentFactory{T}"/> implementation for native csharp console I/O i.e. dotnet.
|
||||
/// This factory creates instances of internal classes <see cref="NetInput"/>, <see cref="NetOutput"/> etc.
|
||||
/// <see cref="IComponentFactory{T}"/> implementation for native csharp console I/O i.e. dotnet.
|
||||
/// This factory creates instances of internal classes <see cref="NetInput"/>, <see cref="NetOutput"/> etc.
|
||||
/// </summary>
|
||||
public class NetComponentFactory : ComponentFactory<ConsoleKeyInfo>
|
||||
public class NetComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override IConsoleInput<ConsoleKeyInfo> CreateInput ()
|
||||
{
|
||||
return new NetInput ();
|
||||
}
|
||||
public override IInput<ConsoleKeyInfo> CreateInput () { return new NetInput (); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConsoleOutput CreateOutput ()
|
||||
{
|
||||
return new NetOutput ();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) { return new NetInputProcessor (inputBuffer); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
|
||||
{
|
||||
return new NetInputProcessor (inputBuffer);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override IOutput CreateOutput () { return new NetOutput (); }
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>.
|
||||
/// <see cref="IInput{TInputRecord}"/> implementation that uses native dotnet methods e.g. <see cref="System.Console"/>.
|
||||
/// The <see cref="Peek"/> and <see cref="Read"/> methods are executed
|
||||
/// on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
|
||||
/// </summary>
|
||||
public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
|
||||
public class NetInput : InputImpl<ConsoleKeyInfo>, ITestableInput<ConsoleKeyInfo>, IDisposable
|
||||
{
|
||||
private readonly NetWinVTConsole _adjustConsole;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the class. Implicitly sends
|
||||
/// console mode settings that enable virtual input (mouse
|
||||
@@ -16,12 +16,7 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
|
||||
/// </summary>
|
||||
public NetInput ()
|
||||
{
|
||||
Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}");
|
||||
|
||||
if (ConsoleDriver.RunningUnitTests)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Logging.Information ($"Creating {nameof (NetInput)}");
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
@@ -34,75 +29,110 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
|
||||
catch (ApplicationException ex)
|
||||
{
|
||||
// Likely running as a unit test, or in a non-interactive session.
|
||||
Logging.Logger.LogCritical (
|
||||
ex,
|
||||
"NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI");
|
||||
Logging.Critical ($"NetWinVTConsole could not configure terminal modes. May indicate running in non-interactive session: {ex}");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Enable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
|
||||
|
||||
//Set cursor key to application.
|
||||
Console.Out.Write (EscSeqUtils.CSI_HideCursor);
|
||||
|
||||
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
|
||||
Console.TreatControlCAsInput = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<ConsoleKeyInfo> 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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
/// <inheritdoc />
|
||||
public void AddInput (ConsoleKeyInfo input) { throw new NotImplementedException (); }
|
||||
|
||||
//Disable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
|
||||
/// <inheritdoc/>
|
||||
public override bool Peek ()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Console.KeyAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Set cursor key to cursor.
|
||||
Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<ConsoleKeyInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,8 @@ namespace Terminal.Gui.Drivers;
|
||||
/// <summary>
|
||||
/// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
|
||||
/// </summary>
|
||||
public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
|
||||
public class NetInputProcessor : InputProcessorImpl<ConsoleKeyInfo>
|
||||
{
|
||||
#pragma warning disable CA2211
|
||||
/// <summary>
|
||||
/// Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests.
|
||||
/// <remarks>
|
||||
/// This makes the task of capturing user/language/terminal specific keyboard issues easier to
|
||||
/// diagnose. By turning this on and searching logs user can send us exactly the input codes that are released
|
||||
/// to input stream.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public static bool GenerateTestCasesForKeyPresses = false;
|
||||
#pragma warning restore CA2211
|
||||
|
||||
/// <inheritdoc/>
|
||||
public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ())
|
||||
{
|
||||
@@ -26,41 +14,11 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Process (ConsoleKeyInfo consoleKeyInfo)
|
||||
protected override void Process (ConsoleKeyInfo input)
|
||||
{
|
||||
// For building test cases
|
||||
if (GenerateTestCasesForKeyPresses)
|
||||
{
|
||||
Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo));
|
||||
}
|
||||
|
||||
foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
|
||||
foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input)))
|
||||
{
|
||||
ProcessAfterParsing (released.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ProcessAfterParsing (ConsoleKeyInfo input)
|
||||
{
|
||||
var key = KeyConverter.ToKey (input);
|
||||
|
||||
// If the key is not valid, we don't want to raise any events.
|
||||
if (IsValidInput (key, out key))
|
||||
{
|
||||
OnKeyDown (key);
|
||||
OnKeyUp (key);
|
||||
}
|
||||
}
|
||||
|
||||
/* For building test cases */
|
||||
private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input)
|
||||
{
|
||||
string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'";
|
||||
|
||||
return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}),";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IKeyConverter{T}"/> capable of converting the
|
||||
@@ -23,4 +22,7 @@ internal class NetKeyConverter : IKeyConverter<ConsoleKeyInfo>
|
||||
|
||||
return EscSeqUtils.MapKey (adjustedInput);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConsoleKeyInfo ToKeyInfo (Key key) { return ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key.KeyCode); }
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IConsoleOutput"/> that uses native dotnet
|
||||
/// Implementation of <see cref="IOutput"/> that uses native dotnet
|
||||
/// methods e.g. <see cref="System.Console"/>
|
||||
/// </summary>
|
||||
public class NetOutput : OutputBase, IConsoleOutput
|
||||
public class NetOutput : OutputBase, IOutput
|
||||
{
|
||||
private readonly bool _isWinPlatform;
|
||||
|
||||
@@ -15,9 +15,16 @@ public class NetOutput : OutputBase, IConsoleOutput
|
||||
/// </summary>
|
||||
public NetOutput ()
|
||||
{
|
||||
Logging.Logger.LogInformation ($"Creating {nameof (NetOutput)}");
|
||||
Logging.Information ($"Creating {nameof (NetOutput)}");
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
try
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore for unit tests
|
||||
}
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
@@ -30,20 +37,36 @@ public class NetOutput : OutputBase, IConsoleOutput
|
||||
/// <inheritdoc/>
|
||||
public void Write (ReadOnlySpan<char> text)
|
||||
{
|
||||
Console.Out.Write (text);
|
||||
try
|
||||
{
|
||||
Console.Out.Write (text);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Not connected to a terminal; do nothing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Size GetSize ()
|
||||
{
|
||||
if (ConsoleDriver.RunningUnitTests)
|
||||
try
|
||||
{
|
||||
// For unit tests, we return a default size.
|
||||
return Size.Empty;
|
||||
Size size = new (Console.WindowWidth, Console.WindowHeight);
|
||||
return size.IsEmpty ? new (80, 25) : size;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Not connected to a terminal; return a default size
|
||||
return new (80, 25);
|
||||
}
|
||||
}
|
||||
|
||||
return new (Console.WindowWidth, Console.WindowHeight);
|
||||
/// <inheritdoc />
|
||||
public Point GetCursorPosition ()
|
||||
{
|
||||
return _lastCursorPosition ?? Point.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -88,7 +111,14 @@ public class NetOutput : OutputBase, IConsoleOutput
|
||||
/// <inheritdoc />
|
||||
protected override void Write (StringBuilder output)
|
||||
{
|
||||
Console.Out.Write (output);
|
||||
try
|
||||
{
|
||||
Console.Out.Write (output);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Not connected to a terminal; do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -103,7 +133,7 @@ public class NetOutput : OutputBase, IConsoleOutput
|
||||
|
||||
if (_isWinPlatform)
|
||||
{
|
||||
// Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
|
||||
// Could happen that the windows is still resizing and the col is bigger than Console.WindowWidth.
|
||||
try
|
||||
{
|
||||
Console.SetCursorPosition (col, row);
|
||||
@@ -131,23 +161,30 @@ public class NetOutput : OutputBase, IConsoleOutput
|
||||
|
||||
private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
|
||||
|
||||
/// <inheritdoc cref="IConsoleOutput.SetCursorVisibility"/>
|
||||
/// <inheritdoc cref="IOutput.SetCursorVisibility"/>
|
||||
public override void SetCursorVisibility (CursorVisibility visibility)
|
||||
{
|
||||
if (visibility != CursorVisibility.Invisible)
|
||||
try
|
||||
{
|
||||
if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
|
||||
if (visibility != CursorVisibility.Invisible)
|
||||
{
|
||||
_currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
|
||||
if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
|
||||
{
|
||||
_currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
|
||||
|
||||
Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
|
||||
Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
|
||||
}
|
||||
|
||||
Write (EscSeqUtils.CSI_ShowCursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write (EscSeqUtils.CSI_HideCursor);
|
||||
}
|
||||
|
||||
Write (EscSeqUtils.CSI_ShowCursor);
|
||||
}
|
||||
else
|
||||
catch
|
||||
{
|
||||
Write (EscSeqUtils.CSI_HideCursor);
|
||||
// Ignore any exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -3,69 +3,108 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// <summary>
|
||||
/// Provides the main implementation of the driver abstraction layer for Terminal.Gui.
|
||||
/// This implementation of <see cref="IDriver"/> coordinates the interaction between input processing, output
|
||||
/// rendering,
|
||||
/// screen size monitoring, and ANSI escape sequence handling.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DriverImpl"/> implements <see cref="IDriver"/>,
|
||||
/// serving as the central coordination point for console I/O operations. It delegates functionality
|
||||
/// to specialized components:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="IInputProcessor"/> - Processes keyboard and mouse input</item>
|
||||
/// <item><see cref="IOutputBuffer"/> - Manages the screen buffer state</item>
|
||||
/// <item><see cref="IOutput"/> - Handles actual console output rendering</item>
|
||||
/// <item><see cref="AnsiRequestScheduler"/> - Manages ANSI escape sequence requests</item>
|
||||
/// <item><see cref="ISizeMonitor"/> - Monitors terminal size changes</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// This class is internal and should not be used directly by application code.
|
||||
/// Applications interact with drivers through the <see cref="Application"/> class.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal class DriverImpl : IDriver
|
||||
{
|
||||
private readonly IConsoleOutput _output;
|
||||
private readonly IOutputBuffer _outputBuffer;
|
||||
private readonly IOutput _output;
|
||||
private readonly AnsiRequestScheduler _ansiRequestScheduler;
|
||||
private CursorVisibility _lastCursor = CursorVisibility.Default;
|
||||
|
||||
/// <summary>
|
||||
/// The event fired when the screen changes (size, position, etc.).
|
||||
/// Initializes a new instance of the <see cref="DriverImpl"/> class.
|
||||
/// </summary>
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
|
||||
public IInputProcessor InputProcessor { get; }
|
||||
public IOutputBuffer OutputBuffer => _outputBuffer;
|
||||
|
||||
public IConsoleSizeMonitor ConsoleSizeMonitor { get; }
|
||||
|
||||
|
||||
public ConsoleDriverFacade (
|
||||
/// <param name="inputProcessor">The input processor for handling keyboard and mouse events.</param>
|
||||
/// <param name="outputBuffer">The output buffer for managing screen state.</param>
|
||||
/// <param name="output">The output interface for rendering to the console.</param>
|
||||
/// <param name="ansiRequestScheduler">The scheduler for managing ANSI escape sequence requests.</param>
|
||||
/// <param name="sizeMonitor">The monitor for tracking terminal size changes.</param>
|
||||
public DriverImpl (
|
||||
IInputProcessor inputProcessor,
|
||||
IOutputBuffer outputBuffer,
|
||||
IConsoleOutput output,
|
||||
IOutput output,
|
||||
AnsiRequestScheduler ansiRequestScheduler,
|
||||
IConsoleSizeMonitor sizeMonitor
|
||||
ISizeMonitor sizeMonitor
|
||||
)
|
||||
{
|
||||
InputProcessor = inputProcessor;
|
||||
_output = output;
|
||||
_outputBuffer = outputBuffer;
|
||||
OutputBuffer = outputBuffer;
|
||||
_ansiRequestScheduler = ansiRequestScheduler;
|
||||
|
||||
InputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e);
|
||||
InputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e);
|
||||
|
||||
InputProcessor.MouseEvent += (s, e) =>
|
||||
{
|
||||
//Logging.Logger.LogTrace ($"Mouse {e.Flags} at x={e.ScreenPosition.X} y={e.ScreenPosition.Y}");
|
||||
MouseEvent?.Invoke (s, e);
|
||||
};
|
||||
|
||||
ConsoleSizeMonitor = sizeMonitor;
|
||||
SizeMonitor = sizeMonitor;
|
||||
|
||||
sizeMonitor.SizeChanged += (_, e) =>
|
||||
{
|
||||
SetScreenSize(e.Size!.Value.Width, e.Size.Value.Height);
|
||||
//SizeChanged?.Invoke (this, e);
|
||||
};
|
||||
{
|
||||
SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height);
|
||||
|
||||
//SizeChanged?.Invoke (this, e);
|
||||
};
|
||||
|
||||
CreateClipboard ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The event fired when the screen changes (size, position, etc.).
|
||||
/// </summary>
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IInputProcessor InputProcessor { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IOutputBuffer OutputBuffer { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISizeMonitor SizeMonitor { get; }
|
||||
|
||||
|
||||
private void CreateClipboard ()
|
||||
{
|
||||
if (FakeDriver.FakeBehaviors.UseFakeClipboard)
|
||||
if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake"))
|
||||
{
|
||||
Clipboard = new FakeClipboard (
|
||||
FakeDriver.FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException,
|
||||
FakeDriver.FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse);
|
||||
if (Clipboard is null)
|
||||
{
|
||||
Clipboard = new FakeClipboard ();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows)
|
||||
{
|
||||
Clipboard = new WindowsClipboard ();
|
||||
}
|
||||
@@ -77,10 +116,8 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
{
|
||||
Clipboard = new WSLClipboard ();
|
||||
}
|
||||
else
|
||||
{
|
||||
Clipboard = new FakeClipboard ();
|
||||
}
|
||||
|
||||
// Clipboard is set to FakeClipboard at initialization
|
||||
}
|
||||
|
||||
/// <summary>Gets the location and size of the terminal screen.</summary>
|
||||
@@ -88,27 +125,27 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ConsoleDriver.RunningUnitTests && _output is WindowsOutput or NetOutput)
|
||||
{
|
||||
// In unit tests, we don't have a real output, so we return an empty rectangle.
|
||||
return Rectangle.Empty;
|
||||
}
|
||||
//if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput)
|
||||
//{
|
||||
// // In unit tests, we don't have a real output, so we return an empty rectangle.
|
||||
// return Rectangle.Empty;
|
||||
//}
|
||||
|
||||
return new (0, 0, _outputBuffer.Cols, _outputBuffer.Rows);
|
||||
return new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the screen size for testing purposes. Only supported by FakeDriver.
|
||||
/// Sets the screen size for testing purposes. Only supported by FakeDriver.
|
||||
/// </summary>
|
||||
/// <param name="width">The new width in columns.</param>
|
||||
/// <param name="height">The new height in rows.</param>
|
||||
/// <exception cref="NotSupportedException">Thrown when called on non-FakeDriver instances.</exception>
|
||||
public virtual void SetScreenSize (int width, int height)
|
||||
{
|
||||
_outputBuffer.SetSize (width, height);
|
||||
OutputBuffer.SetSize (width, height);
|
||||
_output.SetSize (width, height);
|
||||
SizeChanged?.Invoke(this, new (new (width, height)));
|
||||
SizeChanged?.Invoke (this, new (new (width, height)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,24 +155,24 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Region? Clip
|
||||
{
|
||||
get => _outputBuffer.Clip;
|
||||
set => _outputBuffer.Clip = value;
|
||||
get => OutputBuffer.Clip;
|
||||
set => OutputBuffer.Clip = value;
|
||||
}
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
public IClipboard Clipboard { get; private set; } = new FakeClipboard ();
|
||||
public IClipboard? Clipboard { get; private set; } = new FakeClipboard ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Col => _outputBuffer.Col;
|
||||
public int Col => OutputBuffer.Col;
|
||||
|
||||
/// <summary>The number of columns visible in the terminal.</summary>
|
||||
public int Cols
|
||||
{
|
||||
get => _outputBuffer.Cols;
|
||||
set => _outputBuffer.Cols = value;
|
||||
get => OutputBuffer.Cols;
|
||||
set => OutputBuffer.Cols = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,51 +181,51 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// </summary>
|
||||
public Cell [,]? Contents
|
||||
{
|
||||
get => _outputBuffer.Contents;
|
||||
set => _outputBuffer.Contents = value;
|
||||
get => OutputBuffer.Contents;
|
||||
set => OutputBuffer.Contents = value;
|
||||
}
|
||||
|
||||
/// <summary>The leftmost column in the terminal.</summary>
|
||||
public int Left
|
||||
{
|
||||
get => _outputBuffer.Left;
|
||||
set => _outputBuffer.Left = value;
|
||||
get => OutputBuffer.Left;
|
||||
set => OutputBuffer.Left = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Row => _outputBuffer.Row;
|
||||
public int Row => OutputBuffer.Row;
|
||||
|
||||
/// <summary>The number of rows visible in the terminal.</summary>
|
||||
public int Rows
|
||||
{
|
||||
get => _outputBuffer.Rows;
|
||||
set => _outputBuffer.Rows = value;
|
||||
get => OutputBuffer.Rows;
|
||||
set => OutputBuffer.Rows = value;
|
||||
}
|
||||
|
||||
/// <summary>The topmost row in the terminal.</summary>
|
||||
public int Top
|
||||
{
|
||||
get => _outputBuffer.Top;
|
||||
set => _outputBuffer.Top = value;
|
||||
get => OutputBuffer.Top;
|
||||
set => OutputBuffer.Top = value;
|
||||
}
|
||||
|
||||
// TODO: Probably not everyone right?
|
||||
|
||||
/// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
|
||||
/// <summary>Gets whether the <see cref="IDriver"/> supports TrueColor output.</summary>
|
||||
public bool SupportsTrueColor => true;
|
||||
|
||||
// TODO: Currently ignored
|
||||
/// <summary>
|
||||
/// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
|
||||
/// Gets or sets whether the <see cref="IDriver"/> should use 16 colors instead of the default TrueColors.
|
||||
/// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is
|
||||
/// <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
|
||||
/// Will be forced to <see langword="true"/> if <see cref="IDriver.SupportsTrueColor"/> is
|
||||
/// <see langword="false"/>, indicating that the <see cref="IDriver"/> cannot support TrueColor.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool Force16Colors
|
||||
@@ -198,85 +235,86 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
|
||||
/// The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or
|
||||
/// <see cref="AddStr"/>
|
||||
/// call.
|
||||
/// </summary>
|
||||
public Attribute CurrentAttribute
|
||||
{
|
||||
get => _outputBuffer.CurrentAttribute;
|
||||
set => _outputBuffer.CurrentAttribute = value;
|
||||
get => OutputBuffer.CurrentAttribute;
|
||||
set => OutputBuffer.CurrentAttribute = value;
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified rune to the display at the current cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
|
||||
/// When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside of the
|
||||
/// <see cref="ConsoleDriver.Clip"/> or screen
|
||||
/// dimensions defined by <see cref="ConsoleDriver.Cols"/>.
|
||||
/// <see cref="IDriver.Clip"/> or screen
|
||||
/// dimensions defined by <see cref="IDriver.Cols"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number
|
||||
/// If <paramref name="rune"/> requires more than one column, and <see cref="IDriver.Col"/> plus the number
|
||||
/// of columns
|
||||
/// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement
|
||||
/// needed exceeds the <see cref="IDriver.Clip"/> or screen dimensions, the default Unicode replacement
|
||||
/// character (U+FFFD)
|
||||
/// will be added instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="rune">Rune to add.</param>
|
||||
public void AddRune (Rune rune) { _outputBuffer.AddRune (rune); }
|
||||
public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
|
||||
/// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
|
||||
/// convenience method that calls <see cref="IDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
|
||||
/// constructor.
|
||||
/// </summary>
|
||||
/// <param name="c">Character to add.</param>
|
||||
public void AddRune (char c) { _outputBuffer.AddRune (c); }
|
||||
public void AddRune (char c) { OutputBuffer.AddRune (c); }
|
||||
|
||||
/// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/>
|
||||
/// When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="str"/> required, unless the new column value is outside of the <see cref="IDriver.Clip"/>
|
||||
/// or screen
|
||||
/// dimensions defined by <see cref="ConsoleDriver.Cols"/>.
|
||||
/// dimensions defined by <see cref="IDriver.Cols"/>.
|
||||
/// </para>
|
||||
/// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
|
||||
/// </remarks>
|
||||
/// <param name="str">String.</param>
|
||||
public void AddStr (string str) { _outputBuffer.AddStr (str); }
|
||||
public void AddStr (string str) { OutputBuffer.AddStr (str); }
|
||||
|
||||
/// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
|
||||
/// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
|
||||
public void ClearContents ()
|
||||
{
|
||||
_outputBuffer.ClearContents ();
|
||||
OutputBuffer.ClearContents ();
|
||||
ClearedContents?.Invoke (this, new MouseEventArgs ());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking.
|
||||
/// Raised each time <see cref="IDriver.ClearContents"/> is called. For benchmarking.
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs>? ClearedContents;
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
|
||||
/// Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
|
||||
/// The value of <see cref="IDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
|
||||
/// drawn.
|
||||
/// </remarks>
|
||||
/// <param name="rect">The Screen-relative rectangle.</param>
|
||||
/// <param name="rune">The Rune used to fill the rectangle</param>
|
||||
public void FillRect (Rectangle rect, Rune rune = default) { _outputBuffer.FillRect (rect, rune); }
|
||||
public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="c"></param>
|
||||
public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); }
|
||||
public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string GetVersionInfo ()
|
||||
@@ -300,28 +338,28 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// <see langword="false"/> if the coordinate is outside the screen bounds or outside of
|
||||
/// <see cref="ConsoleDriver.Clip"/>.
|
||||
/// <see cref="IDriver.Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
public bool IsValidLocation (Rune rune, int col, int row) { return _outputBuffer.IsValidLocation (rune, col, row); }
|
||||
public bool IsValidLocation (Rune rune, int col, int row) { return OutputBuffer.IsValidLocation (rune, col, row); }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in
|
||||
/// <see cref="ConsoleDriver.Contents"/>.
|
||||
/// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine
|
||||
/// Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
|
||||
/// <see cref="IDriver.Contents"/>.
|
||||
/// Used by <see cref="IDriver.AddRune(System.Text.Rune)"/> and <see cref="IDriver.AddStr"/> to determine
|
||||
/// where to add content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
|
||||
/// <para>
|
||||
/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/>
|
||||
/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="IDriver.Cols"/>
|
||||
/// and
|
||||
/// <see cref="ConsoleDriver.Rows"/>, the method still sets those properties.
|
||||
/// <see cref="IDriver.Rows"/>, the method still sets those properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
public void Move (int col, int row) { _outputBuffer.Move (col, row); }
|
||||
public void Move (int col, int row) { OutputBuffer.Move (col, row); }
|
||||
|
||||
// TODO: Probably part of output
|
||||
|
||||
@@ -347,6 +385,8 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// <inheritdoc/>
|
||||
public void Suspend ()
|
||||
{
|
||||
// BUGBUG: This is all platform-specific and should not be implemented here.
|
||||
// BUGBUG: This needs to be in each platform's driver implementation.
|
||||
if (Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
return;
|
||||
@@ -354,7 +394,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
|
||||
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
|
||||
|
||||
if (!ConsoleDriver.RunningUnitTests)
|
||||
try
|
||||
{
|
||||
Console.ResetColor ();
|
||||
Console.Clear ();
|
||||
@@ -369,16 +409,21 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
|
||||
//Enable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
|
||||
|
||||
Application.LayoutAndDraw ();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Error ($"Error suspending terminal: {ex.Message}");
|
||||
}
|
||||
|
||||
Application.LayoutAndDraw ();
|
||||
|
||||
|
||||
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
|
||||
/// <see cref="ConsoleDriver.Row"/>.
|
||||
/// Sets the position of the terminal cursor to <see cref="IDriver.Col"/> and
|
||||
/// <see cref="IDriver.Row"/>.
|
||||
/// </summary>
|
||||
public void UpdateCursor () { _output.SetCursorPosition (Col, Row); }
|
||||
|
||||
@@ -397,22 +442,22 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
/// <returns>The previously set Attribute.</returns>
|
||||
public Attribute SetAttribute (Attribute newAttribute)
|
||||
{
|
||||
Attribute currentAttribute = _outputBuffer.CurrentAttribute;
|
||||
_outputBuffer.CurrentAttribute = newAttribute;
|
||||
Attribute currentAttribute = OutputBuffer.CurrentAttribute;
|
||||
OutputBuffer.CurrentAttribute = newAttribute;
|
||||
|
||||
return currentAttribute;
|
||||
}
|
||||
|
||||
/// <summary>Gets the current <see cref="Attribute"/>.</summary>
|
||||
/// <returns>The current attribute.</returns>
|
||||
public Attribute GetAttribute () { return _outputBuffer.CurrentAttribute; }
|
||||
public Attribute GetAttribute () { return OutputBuffer.CurrentAttribute; }
|
||||
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
|
||||
public event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>Event fired when a key is released.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="IDriver.KeyDown"/>
|
||||
/// processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
@@ -422,17 +467,31 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="ansi"></param>
|
||||
public void WriteRaw (string ansi) { _output.Write (ansi); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void EnqueueKeyEvent (Key key)
|
||||
{
|
||||
InputProcessor.EnqueueKeyDownEvent (key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the given <paramref name="request"/> for execution
|
||||
/// Queues the specified ANSI escape sequence request for execution.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="request">The ANSI request to queue.</param>
|
||||
/// <remarks>
|
||||
/// The request is sent immediately if possible, or queued for later execution
|
||||
/// by the <see cref="AnsiRequestScheduler"/> to prevent overwhelming the console.
|
||||
/// </remarks>
|
||||
public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AnsiRequestScheduler"/> instance used by this driver.
|
||||
/// </summary>
|
||||
/// <returns>The ANSI request scheduler.</returns>
|
||||
public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -440,4 +499,9 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
{
|
||||
// No need we will always draw when dirty
|
||||
}
|
||||
|
||||
public string? GetName ()
|
||||
{
|
||||
return InputProcessor.DriverName?.ToLowerInvariant ();
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,47 @@ using System.Collections.Concurrent;
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IComponentFactory{T}"/> implementation for fake/mock console I/O used in unit tests.
|
||||
/// This factory creates instances that simulate console behavior without requiring a real terminal.
|
||||
/// <see cref="IComponentFactory{T}"/> implementation for fake/mock console I/O used in unit tests.
|
||||
/// This factory creates instances that simulate console behavior without requiring a real terminal.
|
||||
/// </summary>
|
||||
public class FakeComponentFactory : ComponentFactory<ConsoleKeyInfo>
|
||||
public class FakeComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
|
||||
{
|
||||
private readonly ConcurrentQueue<ConsoleKeyInfo>? _predefinedInput;
|
||||
private readonly FakeConsoleOutput? _output;
|
||||
private readonly FakeInput? _input;
|
||||
private readonly IOutput? _output;
|
||||
private readonly ISizeMonitor? _sizeMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FakeComponentFactory with optional predefined input and output capture.
|
||||
/// Creates a new FakeComponentFactory with optional output capture.
|
||||
/// </summary>
|
||||
/// <param name="predefinedInput">Optional queue of predefined input events to simulate.</param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="output">Optional fake output to capture what would be written to console.</param>
|
||||
public FakeComponentFactory (ConcurrentQueue<ConsoleKeyInfo>? predefinedInput = null, FakeConsoleOutput? output = null)
|
||||
/// <param name="sizeMonitor"></param>
|
||||
public FakeComponentFactory (FakeInput? input = null, IOutput? output = null, ISizeMonitor? sizeMonitor = null)
|
||||
{
|
||||
_predefinedInput = predefinedInput;
|
||||
_input = input;
|
||||
_output = output;
|
||||
_sizeMonitor = sizeMonitor;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
|
||||
{
|
||||
return _sizeMonitor ?? new SizeMonitorImpl (consoleOutput);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IConsoleInput<ConsoleKeyInfo> CreateInput ()
|
||||
public override IInput<ConsoleKeyInfo> CreateInput ()
|
||||
{
|
||||
return new FakeConsoleInput (_predefinedInput);
|
||||
return _input ?? new FakeInput ();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IConsoleOutput CreateOutput ()
|
||||
{
|
||||
return _output ?? new FakeConsoleOutput ();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) { return new FakeInputProcessor (inputBuffer); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
|
||||
/// <inheritdoc/>
|
||||
public override IOutput CreateOutput ()
|
||||
{
|
||||
return new NetInputProcessor (inputBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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 ();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Fake console input for testing that can return predefined input or wait indefinitely.
|
||||
/// </summary>
|
||||
public class FakeConsoleInput : ConsoleInput<ConsoleKeyInfo>
|
||||
{
|
||||
private readonly ConcurrentQueue<ConsoleKeyInfo>? _predefinedInput;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FakeConsoleInput with optional predefined input.
|
||||
/// </summary>
|
||||
/// <param name="predefinedInput">Optional queue of predefined input to return.</param>
|
||||
public FakeConsoleInput (ConcurrentQueue<ConsoleKeyInfo>? predefinedInput = null)
|
||||
{
|
||||
_predefinedInput = predefinedInput;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Peek ()
|
||||
{
|
||||
if (_predefinedInput != null && !_predefinedInput.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// No input available
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<ConsoleKeyInfo> Read ()
|
||||
{
|
||||
if (_predefinedInput != null && _predefinedInput.TryDequeue (out ConsoleKeyInfo key))
|
||||
{
|
||||
yield return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
//
|
||||
// FakeDriver.cs: A fake IConsoleDriver for unit tests.
|
||||
//
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>Implements a mock IConsoleDriver for unit testing</summary>
|
||||
public class FakeDriver : ConsoleDriver
|
||||
{
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
public class Behaviors
|
||||
{
|
||||
public Behaviors (
|
||||
bool useFakeClipboard = false,
|
||||
bool fakeClipboardAlwaysThrowsNotSupportedException = false,
|
||||
bool fakeClipboardIsSupportedAlwaysTrue = false
|
||||
)
|
||||
{
|
||||
UseFakeClipboard = useFakeClipboard;
|
||||
FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
|
||||
FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
|
||||
|
||||
// double check usage is correct
|
||||
Debug.Assert (!useFakeClipboard && !fakeClipboardAlwaysThrowsNotSupportedException);
|
||||
Debug.Assert (!useFakeClipboard && !fakeClipboardIsSupportedAlwaysTrue);
|
||||
}
|
||||
|
||||
public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
|
||||
public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
|
||||
public bool UseFakeClipboard { get; internal set; }
|
||||
}
|
||||
|
||||
public static Behaviors FakeBehaviors { get; } = new ();
|
||||
public override bool SupportsTrueColor => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void WriteRaw (string ansi) { }
|
||||
|
||||
public FakeDriver ()
|
||||
{
|
||||
// FakeDriver implies UnitTests
|
||||
RunningUnitTests = true;
|
||||
|
||||
//base.Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
|
||||
//base.Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
|
||||
|
||||
if (FakeBehaviors.UseFakeClipboard)
|
||||
{
|
||||
Clipboard = new FakeClipboard (
|
||||
FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException,
|
||||
FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
|
||||
{
|
||||
Clipboard = new WindowsClipboard ();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
|
||||
{
|
||||
Clipboard = new MacOSXClipboard ();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PlatformDetection.IsWSLPlatform ())
|
||||
{
|
||||
Clipboard = new WSLClipboard ();
|
||||
}
|
||||
else
|
||||
{
|
||||
Clipboard = new UnixClipboard ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void End ()
|
||||
{
|
||||
FakeConsole.ResetColor ();
|
||||
FakeConsole.Clear ();
|
||||
}
|
||||
|
||||
public override void Init ()
|
||||
{
|
||||
FakeConsole.MockKeyPresses.Clear ();
|
||||
|
||||
//Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
|
||||
//Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
|
||||
FakeConsole.Clear ();
|
||||
|
||||
SetScreenSize (80,25);
|
||||
ResizeScreen ();
|
||||
ClearContents ();
|
||||
CurrentAttribute = new (Color.White, Color.Black);
|
||||
}
|
||||
|
||||
public override bool UpdateScreen ()
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
int savedRow = FakeConsole.CursorTop;
|
||||
int savedCol = FakeConsole.CursorLeft;
|
||||
bool savedCursorVisible = FakeConsole.CursorVisible;
|
||||
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
int rows = Rows;
|
||||
int cols = Cols;
|
||||
var output = new StringBuilder ();
|
||||
var redrawAttr = new Attribute ();
|
||||
int lastCol = -1;
|
||||
|
||||
for (int row = top; row < rows; row++)
|
||||
{
|
||||
if (!_dirtyLines! [row])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
|
||||
FakeConsole.CursorTop = row;
|
||||
FakeConsole.CursorLeft = 0;
|
||||
|
||||
_dirtyLines [row] = false;
|
||||
output.Clear ();
|
||||
|
||||
for (int col = left; col < cols; col++)
|
||||
{
|
||||
lastCol = -1;
|
||||
var outputWidth = 0;
|
||||
|
||||
for (; col < cols; col++)
|
||||
{
|
||||
if (!Contents! [row, col].IsDirty)
|
||||
{
|
||||
if (output.Length > 0)
|
||||
{
|
||||
WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
||||
}
|
||||
else if (lastCol == -1)
|
||||
{
|
||||
lastCol = col;
|
||||
}
|
||||
|
||||
if (lastCol + 1 < cols)
|
||||
{
|
||||
lastCol++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastCol == -1)
|
||||
{
|
||||
lastCol = col;
|
||||
}
|
||||
|
||||
Attribute attr = Contents [row, col].Attribute!.Value;
|
||||
|
||||
// Performance: Only send the escape sequence if the attribute has changed.
|
||||
if (attr != redrawAttr)
|
||||
{
|
||||
redrawAttr = attr;
|
||||
FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 ();
|
||||
FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 ();
|
||||
}
|
||||
|
||||
outputWidth++;
|
||||
Rune rune = Contents [row, col].Rune;
|
||||
output.Append (rune.ToString ());
|
||||
|
||||
if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
|
||||
{
|
||||
WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
||||
FakeConsole.CursorLeft--;
|
||||
}
|
||||
|
||||
Contents [row, col].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
FakeConsole.CursorTop = row;
|
||||
FakeConsole.CursorLeft = lastCol;
|
||||
|
||||
foreach (char c in output.ToString ())
|
||||
{
|
||||
FakeConsole.Write (c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FakeConsole.CursorTop = 0;
|
||||
FakeConsole.CursorLeft = 0;
|
||||
|
||||
//SetCursorVisibility (savedVisibility);
|
||||
|
||||
void WriteToConsole (StringBuilder outputSb, ref int lastColumn, int row, ref int outputWidth)
|
||||
{
|
||||
FakeConsole.CursorTop = row;
|
||||
FakeConsole.CursorLeft = lastColumn;
|
||||
|
||||
foreach (char c in outputSb.ToString ())
|
||||
{
|
||||
FakeConsole.Write (c);
|
||||
}
|
||||
|
||||
outputSb.Clear ();
|
||||
lastColumn += outputWidth;
|
||||
outputWidth = 0;
|
||||
}
|
||||
|
||||
FakeConsole.CursorTop = savedRow;
|
||||
FakeConsole.CursorLeft = savedCol;
|
||||
FakeConsole.CursorVisible = savedCursorVisible;
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private CursorVisibility _savedCursorVisibility;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool GetCursorVisibility (out CursorVisibility visibility)
|
||||
{
|
||||
visibility = FakeConsole.CursorVisible
|
||||
? CursorVisibility.Default
|
||||
: CursorVisibility.Invisible;
|
||||
|
||||
return FakeConsole.CursorVisible;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SetCursorVisibility (CursorVisibility visibility)
|
||||
{
|
||||
_savedCursorVisibility = visibility;
|
||||
|
||||
return FakeConsole.CursorVisible = visibility == CursorVisibility.Default;
|
||||
}
|
||||
|
||||
private bool EnsureCursorVisibility ()
|
||||
{
|
||||
if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
|
||||
{
|
||||
GetCursorVisibility (out CursorVisibility cursorVisibility);
|
||||
_savedCursorVisibility = cursorVisibility;
|
||||
SetCursorVisibility (CursorVisibility.Invisible);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
SetCursorVisibility (_savedCursorVisibility);
|
||||
|
||||
return FakeConsole.CursorVisible;
|
||||
}
|
||||
|
||||
private readonly AnsiResponseParser _parser = new ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override IAnsiResponseParser GetParser () { return _parser; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the screen size for testing purposes. Only available in FakeDriver.
|
||||
/// This method updates the driver's dimensions and triggers the ScreenChanged event.
|
||||
/// </summary>
|
||||
/// <param name="width">The new width in columns.</param>
|
||||
/// <param name="height">The new height in rows.</param>
|
||||
public override void SetScreenSize (int width, int height) { SetBufferSize (width, height); }
|
||||
|
||||
public void SetBufferSize (int width, int height)
|
||||
{
|
||||
FakeConsole.SetBufferSize (width, height);
|
||||
Cols = width;
|
||||
Rows = height;
|
||||
SetConsoleSize (width, height);
|
||||
ProcessResize ();
|
||||
}
|
||||
|
||||
public void SetConsoleSize (int width, int height)
|
||||
{
|
||||
FakeConsole.SetConsoleSize (width, height);
|
||||
FakeConsole.SetBufferSize (width, height);
|
||||
|
||||
if (width != Cols || height != Rows)
|
||||
{
|
||||
SetBufferSize (width, height);
|
||||
Cols = width;
|
||||
Rows = height;
|
||||
}
|
||||
|
||||
ProcessResize ();
|
||||
}
|
||||
|
||||
public void SetWindowPosition (int left, int top)
|
||||
{
|
||||
if (Left > 0 || Top > 0)
|
||||
{
|
||||
Left = 0;
|
||||
Top = 0;
|
||||
}
|
||||
|
||||
FakeConsole.SetWindowPosition (Left, Top);
|
||||
}
|
||||
|
||||
private void ProcessResize ()
|
||||
{
|
||||
ResizeScreen ();
|
||||
ClearContents ();
|
||||
OnSizeChanged (new (new (Cols, Rows)));
|
||||
}
|
||||
|
||||
public virtual void ResizeScreen ()
|
||||
{
|
||||
if (FakeConsole.WindowHeight > 0)
|
||||
{
|
||||
// Can raise an exception while it is still resizing.
|
||||
try
|
||||
{
|
||||
FakeConsole.CursorTop = 0;
|
||||
FakeConsole.CursorLeft = 0;
|
||||
FakeConsole.WindowTop = 0;
|
||||
FakeConsole.WindowLeft = 0;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// CONCURRENCY: Unsynchronized access to Clip is not safe.
|
||||
Clip = new (Screen);
|
||||
}
|
||||
|
||||
public override void UpdateCursor ()
|
||||
{
|
||||
if (!EnsureCursorVisibility ())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevents the exception to size changing during resizing.
|
||||
try
|
||||
{
|
||||
// BUGBUG: Why is this using BufferWidth/Height and now Cols/Rows?
|
||||
if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight)
|
||||
{
|
||||
FakeConsole.SetCursorPosition (Col, Row);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{ }
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{ }
|
||||
}
|
||||
|
||||
#region Not Implemented
|
||||
|
||||
public override void Suspend ()
|
||||
{
|
||||
//throw new NotImplementedException ();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
47
Terminal.Gui/Drivers/FakeDriver/FakeInput.cs
Normal file
47
Terminal.Gui/Drivers/FakeDriver/FakeInput.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IInput{TInputRecord}"/> implementation that uses a fake input source for testing.
|
||||
/// The <see cref="Peek"/> and <see cref="Read"/> methods are executed
|
||||
/// on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
|
||||
/// </summary>
|
||||
public class FakeInput : InputImpl<ConsoleKeyInfo>, ITestableInput<ConsoleKeyInfo>
|
||||
{
|
||||
// Queue for storing injected input that will be returned by Peek/Read
|
||||
private readonly ConcurrentQueue<ConsoleKeyInfo> _testInput = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FakeInput.
|
||||
/// </summary>
|
||||
public FakeInput ()
|
||||
{ }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Peek ()
|
||||
{
|
||||
// Will be called on the input thread.
|
||||
return !_testInput.IsEmpty;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<ConsoleKeyInfo> Read ()
|
||||
{
|
||||
// Will be called on the input thread.
|
||||
while (_testInput.TryDequeue (out ConsoleKeyInfo input))
|
||||
{
|
||||
yield return input;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddInput (ConsoleKeyInfo input)
|
||||
{
|
||||
//Logging.Trace ($"Enqueuing input: {input.Key}");
|
||||
|
||||
// Will be called on the main loop thread.
|
||||
_testInput.Enqueue (input);
|
||||
}
|
||||
}
|
||||
47
Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs
Normal file
47
Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Input processor for <see cref="FakeInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
|
||||
/// </summary>
|
||||
public class FakeInputProcessor : InputProcessorImpl<ConsoleKeyInfo>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public FakeInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ())
|
||||
{
|
||||
DriverName = "fake";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Process (ConsoleKeyInfo input)
|
||||
{
|
||||
Logging.Trace ($"input: {input.KeyChar}");
|
||||
|
||||
foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input)))
|
||||
{
|
||||
Logging.Trace($"released: {released.Item1}");
|
||||
ProcessAfterParsing (released.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void EnqueueMouseEvent (MouseEventArgs mouseEvent)
|
||||
{
|
||||
// FakeDriver uses ConsoleKeyInfo as its input record type, which cannot represent mouse events.
|
||||
|
||||
// If Application.Invoke is available (running in Application context), defer to next iteration
|
||||
// to ensure proper timing - the event is raised after views are laid out.
|
||||
// Otherwise (unit tests), raise immediately so tests can verify synchronously.
|
||||
if (Application.MainThreadId is { })
|
||||
{
|
||||
// Application is running - use Invoke to defer to next iteration
|
||||
Application.Invoke (() => RaiseMouseEvent (mouseEvent));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not in Application context (unit tests) - raise immediately
|
||||
RaiseMouseEvent (mouseEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,43 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Fake console output for testing that captures what would be written to the console.
|
||||
/// </summary>
|
||||
public class FakeConsoleOutput : OutputBase, IConsoleOutput
|
||||
public class FakeOutput : OutputBase, IOutput
|
||||
{
|
||||
private readonly StringBuilder _output = new ();
|
||||
private int _cursorLeft;
|
||||
private int _cursorTop;
|
||||
private Size _consoleSize = new (80, 25);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public FakeOutput ()
|
||||
{
|
||||
LastBuffer = new OutputBufferImpl ();
|
||||
LastBuffer.SetSize (80, 25);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last output buffer written.
|
||||
/// </summary>
|
||||
public IOutputBuffer? LastBuffer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the captured output as a string.
|
||||
/// </summary>
|
||||
public string Output => _output.ToString ();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Point GetCursorPosition ()
|
||||
{
|
||||
return new (_cursorLeft, _cursorTop);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); }
|
||||
|
||||
@@ -34,23 +56,23 @@ public class FakeConsoleOutput : OutputBase, IConsoleOutput
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the fake window size.
|
||||
/// </summary>
|
||||
public void SetConsoleSize (int width, int height) { _consoleSize = new (width, height); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current cursor position.
|
||||
/// </summary>
|
||||
public (int left, int top) GetCursorPosition () { return (_cursorLeft, _cursorTop); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Size GetSize () { return _consoleSize; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Write (ReadOnlySpan<char> text) { _output.Append (text); }
|
||||
public void Write (ReadOnlySpan<char> text)
|
||||
{
|
||||
_output.Append (text);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc cref="IDriver"/>
|
||||
public override void Write (IOutputBuffer buffer)
|
||||
{
|
||||
LastBuffer = buffer;
|
||||
base.Write (buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDriver"/>
|
||||
public override void SetCursorVisibility (CursorVisibility visibility)
|
||||
{
|
||||
// Capture but don't act on it in fake output
|
||||
@@ -70,5 +92,8 @@ public class FakeConsoleOutput : OutputBase, IConsoleOutput
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Write (StringBuilder output) { _output.Append (output); }
|
||||
protected override void Write (StringBuilder output)
|
||||
{
|
||||
_output.Append (output);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,60 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using Terminal.Gui.App;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Base untyped interface for <see cref="IComponentFactory{T}"/> for methods that are not templated on low level
|
||||
/// console input type.
|
||||
/// Base untyped interface for <see cref="IComponentFactory{T}"/> for methods that are not templated on low level
|
||||
/// console input type.
|
||||
/// </summary>
|
||||
public interface IComponentFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the <see cref="IConsoleOutput"/> class for the current driver implementation i.e. the class responsible for
|
||||
/// rendering <see cref="IOutputBuffer"/> into the console.
|
||||
/// Create the <see cref="IOutput"/> class for the current driver implementation i.e. the class responsible for
|
||||
/// rendering <see cref="IOutputBuffer"/> into the console.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IConsoleOutput CreateOutput ();
|
||||
IOutput CreateOutput ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates driver specific subcomponent classes (<see cref="IConsoleInput{T}"/>, <see cref="IInputProcessor"/> etc) for a
|
||||
/// <see cref="IMainLoopCoordinator"/>.
|
||||
/// Creates driver specific subcomponent classes (<see cref="IInput{TInputRecord}"/>, <see cref="IInputProcessor"/>
|
||||
/// etc) for a
|
||||
/// <see cref="IMainLoopCoordinator"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IComponentFactory<T> : IComponentFactory
|
||||
/// <typeparam name="TInputRecord">
|
||||
/// The platform specific console input type. Must be a value type (struct).
|
||||
/// Valid types are <see cref="ConsoleKeyInfo"/>, <see cref="WindowsConsole.InputRecord"/>, and <see cref="char"/>.
|
||||
/// </typeparam>
|
||||
public interface IComponentFactory<TInputRecord> : IComponentFactory
|
||||
where TInputRecord : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Create <see cref="IConsoleInput{T}"/> class for the current driver implementation i.e. the class responsible for reading
|
||||
/// user input from the console.
|
||||
/// Create <see cref="IInput{T}"/> class for the current driver implementation i.e. the class responsible for reading
|
||||
/// user input from the console.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IConsoleInput<T> CreateInput ();
|
||||
IInput<TInputRecord> CreateInput ();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="InputProcessor{T}"/> class for the current driver implementation i.e. the class responsible for
|
||||
/// translating raw console input into Terminal.Gui common event <see cref="Key"/> and <see cref="MouseEventArgs"/>.
|
||||
/// Creates the <see cref="InputProcessorImpl{T}"/> class for the current driver implementation i.e. the class
|
||||
/// responsible for
|
||||
/// translating raw console input into Terminal.Gui common event <see cref="Key"/> and <see cref="MouseEventArgs"/>.
|
||||
/// </summary>
|
||||
/// <param name="inputBuffer"></param>
|
||||
/// <param name="inputQueue">
|
||||
/// The input queue containing raw console input events, populated by <see cref="IInput{TInputRecord}"/>
|
||||
/// implementations on the input thread and
|
||||
/// read by <see cref="IInputProcessor"/> on the main loop thread.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
IInputProcessor CreateInputProcessor (ConcurrentQueue<T> inputBuffer);
|
||||
IInputProcessor CreateInputProcessor (ConcurrentQueue<TInputRecord> inputQueue);
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="IConsoleSizeMonitor"/> class for the current driver implementation i.e. the class responsible for
|
||||
/// reporting the current size of the terminal.
|
||||
/// Creates <see cref="ISizeMonitor"/> class for the current driver implementation i.e. the class responsible for
|
||||
/// reporting the current size of the terminal.
|
||||
/// </summary>
|
||||
/// <param name="consoleOutput"></param>
|
||||
/// <param name="outputBuffer"></param>
|
||||
/// <returns></returns>
|
||||
IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer);
|
||||
ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for v2 driver abstraction layer
|
||||
/// </summary>
|
||||
public interface IConsoleDriverFacade
|
||||
{
|
||||
/// <summary>
|
||||
/// Class responsible for processing native driver input objects
|
||||
/// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
|
||||
/// and detecting and processing ansi escape sequences.
|
||||
/// </summary>
|
||||
IInputProcessor InputProcessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes the desired screen state. Data source for <see cref="IConsoleOutput"/>.
|
||||
/// </summary>
|
||||
IOutputBuffer OutputBuffer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Interface for classes responsible for reporting the current
|
||||
/// size of the terminal window.
|
||||
/// </summary>
|
||||
IConsoleSizeMonitor ConsoleSizeMonitor { get; }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for reading console input indefinitely -
|
||||
/// i.e. in an infinite loop. The class is responsible only
|
||||
/// for reading and storing the input in a thread safe input buffer
|
||||
/// which is then processed downstream e.g. on main UI thread.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IConsoleInput<T> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the input with a buffer into which to put data read
|
||||
/// </summary>
|
||||
/// <param name="inputBuffer"></param>
|
||||
void Initialize (ConcurrentQueue<T> inputBuffer);
|
||||
|
||||
/// <summary>
|
||||
/// Runs in an infinite input loop.
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Raised when token is
|
||||
/// cancelled. This is the only means of exiting the input.
|
||||
/// </exception>
|
||||
void Run (CancellationToken token);
|
||||
}
|
||||
@@ -2,13 +2,37 @@
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>Base interface for Terminal.Gui ConsoleDriver implementations.</summary>
|
||||
/// <summary>Base interface for Terminal.Gui Driver implementations.</summary>
|
||||
/// <remarks>
|
||||
/// There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver
|
||||
/// </remarks>
|
||||
public interface IConsoleDriver
|
||||
public interface IDriver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the driver implementation.
|
||||
/// </summary>
|
||||
string? GetName ();
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for processing native driver input objects
|
||||
/// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
|
||||
/// and detecting and processing ansi escape sequences.
|
||||
/// </summary>
|
||||
IInputProcessor InputProcessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes the desired screen state. Data source for <see cref="IOutput"/>.
|
||||
/// </summary>
|
||||
IOutputBuffer OutputBuffer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Interface for classes responsible for reporting the current
|
||||
/// size of the terminal window.
|
||||
/// </summary>
|
||||
ISizeMonitor SizeMonitor { get; }
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
///
|
||||
IClipboard? Clipboard { get; }
|
||||
|
||||
/// <summary>Gets the location and size of the terminal screen.</summary>
|
||||
@@ -62,17 +86,17 @@ public interface IConsoleDriver
|
||||
/// <summary>The topmost row in the terminal.</summary>
|
||||
int Top { get; set; }
|
||||
|
||||
/// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
|
||||
/// <summary>Gets whether the <see cref="IDriver"/> supports TrueColor output.</summary>
|
||||
bool SupportsTrueColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
|
||||
/// Gets or sets whether the <see cref="IDriver"/> should use 16 colors instead of the default TrueColors.
|
||||
/// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is
|
||||
/// <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
|
||||
/// Will be forced to <see langword="true"/> if <see cref="IDriver.SupportsTrueColor"/> is
|
||||
/// <see langword="false"/>, indicating that the <see cref="IDriver"/> cannot support TrueColor.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool Force16Colors { get; set; }
|
||||
@@ -88,7 +112,7 @@ public interface IConsoleDriver
|
||||
string GetVersionInfo ();
|
||||
|
||||
/// <summary>
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="ansi"></param>
|
||||
void WriteRaw (string ansi);
|
||||
@@ -107,23 +131,23 @@ public interface IConsoleDriver
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// <see langword="false"/> if the coordinate is outside the screen bounds or outside of
|
||||
/// <see cref="ConsoleDriver.Clip"/>.
|
||||
/// <see cref="IDriver.Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
bool IsValidLocation (Rune rune, int col, int row);
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in
|
||||
/// <see cref="ConsoleDriver.Contents"/>.
|
||||
/// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine
|
||||
/// Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
|
||||
/// <see cref="IDriver.Contents"/>.
|
||||
/// Used by <see cref="IDriver.AddRune(System.Text.Rune)"/> and <see cref="IDriver.AddStr"/> to determine
|
||||
/// where to add content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
|
||||
/// <para>
|
||||
/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/>
|
||||
/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="IDriver.Cols"/>
|
||||
/// and
|
||||
/// <see cref="ConsoleDriver.Rows"/>, the method still sets those properties.
|
||||
/// <see cref="IDriver.Rows"/>, the method still sets those properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
@@ -133,15 +157,15 @@ public interface IConsoleDriver
|
||||
/// <summary>Adds the specified rune to the display at the current cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
|
||||
/// When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside of the
|
||||
/// <see cref="ConsoleDriver.Clip"/> or screen
|
||||
/// dimensions defined by <see cref="ConsoleDriver.Cols"/>.
|
||||
/// <see cref="IDriver.Clip"/> or screen
|
||||
/// dimensions defined by <see cref="IDriver.Cols"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number
|
||||
/// If <paramref name="rune"/> requires more than one column, and <see cref="IDriver.Col"/> plus the number
|
||||
/// of columns
|
||||
/// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement
|
||||
/// needed exceeds the <see cref="IDriver.Clip"/> or screen dimensions, the default Unicode replacement
|
||||
/// character (U+FFFD)
|
||||
/// will be added instead.
|
||||
/// </para>
|
||||
@@ -151,7 +175,7 @@ public interface IConsoleDriver
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
|
||||
/// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
|
||||
/// convenience method that calls <see cref="IDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
|
||||
/// constructor.
|
||||
/// </summary>
|
||||
/// <param name="c">Character to add.</param>
|
||||
@@ -160,27 +184,27 @@ public interface IConsoleDriver
|
||||
/// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/>
|
||||
/// When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="str"/> required, unless the new column value is outside of the <see cref="IDriver.Clip"/>
|
||||
/// or screen
|
||||
/// dimensions defined by <see cref="ConsoleDriver.Cols"/>.
|
||||
/// dimensions defined by <see cref="IDriver.Cols"/>.
|
||||
/// </para>
|
||||
/// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
|
||||
/// </remarks>
|
||||
/// <param name="str">String.</param>
|
||||
void AddStr (string str);
|
||||
|
||||
/// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
|
||||
/// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
|
||||
void ClearContents ();
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
|
||||
/// Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
|
||||
/// </summary>
|
||||
event EventHandler<EventArgs> ClearedContents;
|
||||
|
||||
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary>
|
||||
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/></summary>
|
||||
/// <remarks>
|
||||
/// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
|
||||
/// The value of <see cref="IDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
|
||||
/// drawn.
|
||||
/// </remarks>
|
||||
/// <param name="rect">The Screen-relative rectangle.</param>
|
||||
@@ -189,7 +213,7 @@ public interface IConsoleDriver
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="c"></param>
|
||||
@@ -220,8 +244,8 @@ public interface IConsoleDriver
|
||||
void Suspend ();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
|
||||
/// <see cref="ConsoleDriver.Row"/>.
|
||||
/// Sets the position of the terminal cursor to <see cref="IDriver.Col"/> and
|
||||
/// <see cref="IDriver.Row"/>.
|
||||
/// </summary>
|
||||
void UpdateCursor ();
|
||||
|
||||
@@ -243,17 +267,23 @@ public interface IConsoleDriver
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
|
||||
event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>Event fired when a key is released.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="IDriver.KeyDown"/>
|
||||
/// processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
event EventHandler<Key>? KeyUp;
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a key input event to the driver. For unit tests.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
void EnqueueKeyEvent (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Queues the given <paramref name="request"/> for execution
|
||||
/// </summary>
|
||||
172
Terminal.Gui/Drivers/IInput.cs
Normal file
172
Terminal.Gui/Drivers/IInput.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for reading console input in a perpetual loop on a dedicated input thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementations run on a separate thread (started by
|
||||
/// <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>)
|
||||
/// and continuously read platform-specific input from the console, placing it into a thread-safe queue
|
||||
/// for processing by <see cref="IInputProcessor"/> on the main UI thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Architecture:</b>
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// Input Thread: Main UI Thread:
|
||||
/// ┌─────────────────┐ ┌──────────────────────┐
|
||||
/// │ IInput.Run() │ │ IInputProcessor │
|
||||
/// │ ├─ Peek() │ │ ├─ ProcessQueue() │
|
||||
/// │ ├─ Read() │──Enqueue──→ │ ├─ Process() │
|
||||
/// │ └─ Enqueue │ │ ├─ ToKey() │
|
||||
/// └─────────────────┘ │ └─ Raise Events │
|
||||
/// └──────────────────────┘
|
||||
/// </code>
|
||||
/// <para>
|
||||
/// <b>Lifecycle:</b>
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><see cref="Initialize"/> - Set the shared input queue</item>
|
||||
/// <item><see cref="Run"/> - Start the perpetual read loop (blocks until cancelled)</item>
|
||||
/// <item>
|
||||
/// Loop calls <see cref="InputImpl{TInputRecord}.Peek"/> and <see cref="InputImpl{TInputRecord}.Read"/>
|
||||
/// </item>
|
||||
/// <item>Cancellation via `runCancellationToken` or <see cref="ExternalCancellationTokenSource"/></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <b>Implementations:</b>
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="WindowsInput"/> - Uses Windows Console API (<c>ReadConsoleInput</c>)</item>
|
||||
/// <item><see cref="NetInput"/> - Uses .NET <see cref="System.Console"/> API</item>
|
||||
/// <item><see cref="UnixInput"/> - Uses Unix terminal APIs</item>
|
||||
/// <item><see cref="FakeInput"/> - For testing, implements <see cref="ITestableInput{TInputRecord}"/></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <b>Testing Support:</b> See <see cref="ITestableInput{TInputRecord}"/> for programmatic input injection
|
||||
/// in test scenarios.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="TInputRecord">
|
||||
/// The platform-specific input record type:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ConsoleKeyInfo"/> - for .NET and Fake drivers</item>
|
||||
/// <item><see cref="WindowsConsole.InputRecord"/> - for Windows driver</item>
|
||||
/// <item><see cref="char"/> - for Unix driver</item>
|
||||
/// </list>
|
||||
/// </typeparam>
|
||||
public interface IInput<TInputRecord> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an external cancellation token source that can stop the <see cref="Run"/> loop
|
||||
/// in addition to the `runCancellationToken` passed to <see cref="Run"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property allows external code (e.g., test harnesses like <c>GuiTestContext</c>) to
|
||||
/// provide additional cancellation signals such as timeouts or hard-stop conditions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ownership:</b> The setter does NOT transfer ownership of the <see cref="CancellationTokenSource"/>.
|
||||
/// The creator is responsible for disposal. <see cref="IInput{TInputRecord}"/> implementations
|
||||
/// should NOT dispose this token source.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>How it works:</b> <see cref="InputImpl{TInputRecord}.Run"/> creates a linked token that
|
||||
/// responds to BOTH the `runCancellationToken` AND this external token:
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
/// runCancellationToken,
|
||||
/// ExternalCancellationTokenSource.Token);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// Test scenario with timeout:
|
||||
/// <code>
|
||||
/// var input = new FakeInput();
|
||||
/// input.ExternalCancellationTokenSource = new CancellationTokenSource(
|
||||
/// TimeSpan.FromSeconds(30)); // 30-second timeout
|
||||
///
|
||||
/// // Run will stop if either:
|
||||
/// // 1. runCancellationToken is cancelled (normal shutdown)
|
||||
/// // 2. 30 seconds elapse (timeout)
|
||||
/// input.Run(normalCancellationToken);
|
||||
/// </code>
|
||||
/// </example>
|
||||
CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the input reader with the thread-safe queue where read input will be stored.
|
||||
/// </summary>
|
||||
/// <param name="inputQueue">
|
||||
/// The shared <see cref="ConcurrentQueue{T}"/> that both <see cref="Run"/> (producer)
|
||||
/// and <see cref="IInputProcessor"/> (consumer) use for passing input records between threads.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This queue is created by <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/>
|
||||
/// and shared between the input thread and main UI thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Must be called before <see cref="Run"/>.</b> Calling <see cref="Run"/> without
|
||||
/// initialization will throw an exception.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void Initialize (ConcurrentQueue<TInputRecord> inputQueue);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the input loop, continuously reading input and placing it into the queue
|
||||
/// provided by <see cref="Initialize"/>.
|
||||
/// </summary>
|
||||
/// <param name="runCancellationToken">
|
||||
/// The primary cancellation token that stops the input loop. Provided by
|
||||
/// <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/> and triggered
|
||||
/// during application shutdown.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Threading:</b> This method runs on a dedicated input thread created by
|
||||
/// <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>. and blocks until
|
||||
/// cancellation is requested. It should never be called from the main UI thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Cancellation:</b> The loop stops when either <paramref name="runCancellationToken"/>
|
||||
/// or <see cref="ExternalCancellationTokenSource"/> (if set) is cancelled.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Base Implementation:</b> <see cref="InputImpl{TInputRecord}.Run"/> provides the
|
||||
/// standard loop logic:
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// while (!cancelled)
|
||||
/// {
|
||||
/// while (Peek()) // Check for available input
|
||||
/// {
|
||||
/// foreach (var input in Read()) // Read all available
|
||||
/// {
|
||||
/// inputQueue.Enqueue(input); // Store for processing
|
||||
/// }
|
||||
/// }
|
||||
/// Task.Delay(20ms); // Throttle to ~50 polls/second
|
||||
/// }
|
||||
/// </code>
|
||||
/// <para>
|
||||
/// <b>Testing:</b> For <see cref="ITestableInput{TInputRecord}"/> implementations,
|
||||
/// test input injected via <see cref="ITestableInput{TInputRecord}.AddInput"/>
|
||||
/// flows through the same <c>Peek/Read</c> pipeline.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Thrown when <paramref name="runCancellationToken"/> or <see cref="ExternalCancellationTokenSource"/>
|
||||
/// is cancelled. This is the normal/expected means of exiting the input loop.
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if <see cref="Initialize"/> was not called before <see cref="Run"/>.
|
||||
/// </exception>
|
||||
void Run (CancellationToken runCancellationToken);
|
||||
}
|
||||
@@ -3,58 +3,22 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for main loop class that will process the queued input buffer contents.
|
||||
/// Interface for main loop class that will process the queued input.
|
||||
/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
|
||||
/// events and data models.
|
||||
/// </summary>
|
||||
public interface IInputProcessor
|
||||
{
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
|
||||
event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>Event fired when a key is released.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
event EventHandler<Key>? KeyUp;
|
||||
|
||||
/// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
|
||||
/// <summary>Event raised when a terminal sequence read from input is not recognized and therefore ignored.</summary>
|
||||
public event EventHandler<string>? AnsiSequenceSwallowed;
|
||||
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the driver associated with this input processor.
|
||||
/// </summary>
|
||||
string? DriverName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
|
||||
/// <see cref="OnKeyUp"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key event data.</param>
|
||||
void OnKeyDown (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key is released. Fires the <see cref="KeyUp"/> event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
|
||||
/// is complete.
|
||||
/// </remarks>
|
||||
/// <param name="key">The key event data.</param>
|
||||
void OnKeyUp (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.
|
||||
/// </summary>
|
||||
/// <param name="mouseEventArgs">The mouse event data.</param>
|
||||
void OnMouseEvent (MouseEventArgs mouseEventArgs);
|
||||
|
||||
/// <summary>
|
||||
/// Drains the input buffer, processing all available keystrokes
|
||||
/// Drains the input queue, processing all available keystrokes. To be called on the main loop thread.
|
||||
/// </summary>
|
||||
void ProcessQueue ();
|
||||
|
||||
@@ -74,4 +38,59 @@ public interface IInputProcessor
|
||||
/// <see langword="false"/>.
|
||||
/// </returns>
|
||||
bool IsValidInput (Key key, out Key result);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key down event has been dequeued. Raises the <see cref="KeyDown"/> event. This is a precursor to
|
||||
/// <see cref="RaiseKeyUpEvent"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key event data.</param>
|
||||
void RaiseKeyDownEvent (Key key);
|
||||
|
||||
/// <summary>Event raised when a key down event has been dequeued. This is a precursor to <see cref="KeyUp"/>.</summary>
|
||||
event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a key up event to the input queue. For unit tests.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
void EnqueueKeyDownEvent (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key up event has been dequeued. Raises the <see cref="KeyUp"/> event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will call this method after <see cref="RaiseKeyDownEvent"/> processing
|
||||
/// is complete.
|
||||
/// </remarks>
|
||||
/// <param name="key">The key event data.</param>
|
||||
void RaiseKeyUpEvent (Key key);
|
||||
|
||||
/// <summary>Event raised when a key up event has been dequeued.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
event EventHandler<Key>? KeyUp;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a key up event to the input queue. For unit tests.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
void EnqueueKeyUpEvent (Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a mouse event has been dequeued. Raises the <see cref="MouseEvent"/> event.
|
||||
/// </summary>
|
||||
/// <param name="mouseEventArgs">The mouse event data.</param>
|
||||
void RaiseMouseEvent (MouseEventArgs mouseEventArgs);
|
||||
|
||||
/// <summary>Event raised when a mouse event has been dequeued.</summary>
|
||||
event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a mouse input event to the input queue. For unit tests.
|
||||
/// </summary>
|
||||
/// <param name="mouseEvent"></param>
|
||||
void EnqueueMouseEvent (MouseEventArgs mouseEvent);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for subcomponent of a <see cref="InputProcessor{T}"/> which
|
||||
/// Interface for subcomponent of a <see cref="InputProcessorImpl{T}"/> which
|
||||
/// can translate the raw console input type T (which typically varies by
|
||||
/// driver) to the shared Terminal.Gui <see cref="Key"/> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IKeyConverter<in T>
|
||||
/// <typeparam name="TInputRecord"></typeparam>
|
||||
public interface IKeyConverter<TInputRecord>
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the native keyboard class read from console into
|
||||
/// the shared <see cref="Key"/> class used by Terminal.Gui views.
|
||||
/// Converts the native keyboard info type into
|
||||
/// the <see cref="Key"/> class used by Terminal.Gui views.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="keyInfo"></param>
|
||||
/// <returns></returns>
|
||||
Key ToKey (T value);
|
||||
Key ToKey (TInputRecord keyInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="Key"/> into the native keyboard info type. Should be used for simulating
|
||||
/// key presses in unit tests.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
TInputRecord ToKeyInfo (Key key);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for writing console output
|
||||
/// The low-level interface drivers implement to provide output capabilities; encapsulates platform-specific
|
||||
/// output functionality.
|
||||
/// </summary>
|
||||
public interface IConsoleOutput : IDisposable
|
||||
public interface IOutput : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current position of the console cursor.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Point GetCursorPosition ();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current size of the console in rows/columns (i.e.
|
||||
/// of characters not pixels).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Size GetSize ();
|
||||
|
||||
/// <summary>
|
||||
/// Moves the console cursor to the given location.
|
||||
/// </summary>
|
||||
/// <param name="col"></param>
|
||||
/// <param name="row"></param>
|
||||
void SetCursorPosition (int col, int row);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the console cursor (the blinking underscore) to be hidden,
|
||||
/// visible etc.
|
||||
/// </summary>
|
||||
/// <param name="visibility"></param>
|
||||
void SetCursorVisibility (CursorVisibility visibility);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the size of the console.
|
||||
/// </summary>
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
void SetSize (int width, int height);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the given text directly to the console. Use to send
|
||||
/// ansi escape codes etc. Regular screen output should use the
|
||||
@@ -18,32 +53,4 @@ public interface IConsoleOutput : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="buffer"></param>
|
||||
void Write (IOutputBuffer buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current size of the console in rows/columns (i.e.
|
||||
/// of characters not pixels).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Size GetSize ();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the console cursor (the blinking underscore) to be hidden,
|
||||
/// visible etc.
|
||||
/// </summary>
|
||||
/// <param name="visibility"></param>
|
||||
void SetCursorVisibility (CursorVisibility visibility);
|
||||
|
||||
/// <summary>
|
||||
/// Moves the console cursor to the given location.
|
||||
/// </summary>
|
||||
/// <param name="col"></param>
|
||||
/// <param name="row"></param>
|
||||
void SetCursorPosition (int col, int row);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the size of the console..
|
||||
/// </summary>
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
void SetSize (int width, int height);
|
||||
}
|
||||
@@ -3,66 +3,27 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the screen state that you want the console to be in.
|
||||
/// Is designed to be drawn to repeatedly then manifest into the console
|
||||
/// once at the end of iteration after all drawing is finalized.
|
||||
/// Represents the desired screen state for console rendering. This interface provides methods for building up
|
||||
/// visual content (text, attributes, fills) in a buffer that can be efficiently written to the terminal
|
||||
/// in a single operation at the end of each iteration. Final output is handled by <see cref="IOutput"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="IOutputBuffer"/> acts as an intermediary between Terminal.Gui's high-level drawing operations
|
||||
/// and the low-level console output. Rather than writing directly to the console for each operation, views
|
||||
/// draw to this buffer during layout and rendering. The buffer is then flushed to the terminal by
|
||||
/// <see cref="IOutput"/> after all drawing is complete, minimizing flicker and improving performance.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The buffer maintains a 2D array of <see cref="Cell"/> objects in <see cref="Contents"/>, where each cell
|
||||
/// represents a single character position on screen with its associated character, attributes, and dirty state.
|
||||
/// Drawing operations like <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> modify cells at the
|
||||
/// current cursor position (tracked by <see cref="Col"/> and <see cref="Row"/>), respecting any active
|
||||
/// <see cref="Clip"/> region.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IOutputBuffer
|
||||
{
|
||||
/// <summary>
|
||||
/// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called.
|
||||
/// </summary>
|
||||
Cell [,]? Contents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
|
||||
/// to.
|
||||
/// </summary>
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Region? Clip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call.
|
||||
/// </summary>
|
||||
Attribute CurrentAttribute { get; set; }
|
||||
|
||||
/// <summary>The number of rows visible in the terminal.</summary>
|
||||
int Rows { get; set; }
|
||||
|
||||
/// <summary>The number of columns visible in the terminal.</summary>
|
||||
int Cols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Row { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Col { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The first cell index on left of screen - basically always 0.
|
||||
/// Changing this may have unexpected consequences.
|
||||
/// </summary>
|
||||
int Left { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first cell index on top of screen - basically always 0.
|
||||
/// Changing this may have unexpected consequences.
|
||||
/// </summary>
|
||||
int Top { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the column and row to the specified location in the buffer.
|
||||
/// </summary>
|
||||
/// <param name="col">The column to move to.</param>
|
||||
/// <param name="row">The row to move to.</param>
|
||||
void Move (int col, int row);
|
||||
|
||||
/// <summary>Adds the specified rune to the display at the current cursor position.</summary>
|
||||
/// <param name="rune">Rune to add.</param>
|
||||
void AddRune (Rune rune);
|
||||
@@ -82,22 +43,30 @@ public interface IOutputBuffer
|
||||
void ClearContents ();
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the specified coordinate is valid for drawing the specified Rune.
|
||||
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
|
||||
/// to.
|
||||
/// </summary>
|
||||
/// <param name="rune">Used to determine if one or two columns are required.</param>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// True if the coordinate is valid for the Rune; false otherwise.
|
||||
/// </returns>
|
||||
bool IsValidLocation (Rune rune, int col, int row);
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Region? Clip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes the size of the buffer to the given size
|
||||
/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
/// <param name="cols"></param>
|
||||
/// <param name="rows"></param>
|
||||
void SetSize (int cols, int rows);
|
||||
public int Col { get; }
|
||||
|
||||
/// <summary>The number of columns visible in the terminal.</summary>
|
||||
int Cols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called.
|
||||
/// </summary>
|
||||
Cell [,]? Contents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call.
|
||||
/// </summary>
|
||||
Attribute CurrentAttribute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fills the given <paramref name="rect"/> with the given
|
||||
@@ -114,4 +83,50 @@ public interface IOutputBuffer
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="rune"></param>
|
||||
void FillRect (Rectangle rect, char rune);
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the specified coordinate is valid for drawing the specified Rune.
|
||||
/// </summary>
|
||||
/// <param name="rune">Used to determine if one or two columns are required.</param>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <returns>
|
||||
/// True if the coordinate is valid for the Rune; false otherwise.
|
||||
/// </returns>
|
||||
bool IsValidLocation (Rune rune, int col, int row);
|
||||
|
||||
/// <summary>
|
||||
/// The first cell index on left of screen - basically always 0.
|
||||
/// Changing this may have unexpected consequences.
|
||||
/// </summary>
|
||||
int Left { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the column and row to the specified location in the buffer.
|
||||
/// </summary>
|
||||
/// <param name="col">The column to move to.</param>
|
||||
/// <param name="row">The row to move to.</param>
|
||||
void Move (int col, int row);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
/// </summary>
|
||||
public int Row { get; }
|
||||
|
||||
/// <summary>The number of rows visible in the terminal.</summary>
|
||||
int Rows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes the size of the buffer to the given size
|
||||
/// </summary>
|
||||
/// <param name="cols"></param>
|
||||
/// <param name="rows"></param>
|
||||
void SetSize (int cols, int rows);
|
||||
|
||||
/// <summary>
|
||||
/// The first cell index on top of screen - basically always 0.
|
||||
/// Changing this may have unexpected consequences.
|
||||
/// </summary>
|
||||
int Top { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Terminal.Gui.Drivers;
|
||||
/// Interface for classes responsible for reporting the current
|
||||
/// size of the terminal window.
|
||||
/// </summary>
|
||||
public interface IConsoleSizeMonitor
|
||||
public interface ISizeMonitor
|
||||
{
|
||||
/// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
|
||||
event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
15
Terminal.Gui/Drivers/ITestableInput.cs
Normal file
15
Terminal.Gui/Drivers/ITestableInput.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for IInput implementations that support test input injection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInputRecord">The input record type</typeparam>
|
||||
public interface ITestableInput<TInputRecord> : IInput<TInputRecord>
|
||||
where TInputRecord : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an input record that will be returned by Peek/Read for testing.
|
||||
/// </summary>
|
||||
void AddInput (TInputRecord input);
|
||||
}
|
||||
|
||||
89
Terminal.Gui/Drivers/InputImpl.cs
Normal file
89
Terminal.Gui/Drivers/InputImpl.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for reading console input in perpetual loop.
|
||||
/// The <see cref="Peek"/> and <see cref="Read"/> methods are executed
|
||||
/// on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInputRecord"></typeparam>
|
||||
public abstract class InputImpl<TInputRecord> : IInput<TInputRecord>
|
||||
{
|
||||
private ConcurrentQueue<TInputRecord>? _inputQueue;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how to get the current system type, adjust
|
||||
/// in unit tests to simulate specific timings.
|
||||
/// </summary>
|
||||
public Func<DateTime> Now { get; set; } = () => DateTime.Now;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize (ConcurrentQueue<TInputRecord> inputQueue) { _inputQueue = inputQueue; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Run (CancellationToken runCancellationToken)
|
||||
{
|
||||
// Create a linked token source if we have an external one
|
||||
CancellationTokenSource? linkedCts = null;
|
||||
CancellationToken effectiveToken = runCancellationToken;
|
||||
|
||||
if (ExternalCancellationTokenSource != null)
|
||||
{
|
||||
linkedCts = CancellationTokenSource.CreateLinkedTokenSource (runCancellationToken, ExternalCancellationTokenSource.Token);
|
||||
effectiveToken = linkedCts.Token;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_inputQueue == null)
|
||||
{
|
||||
throw new ("Cannot run input before Initialization");
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
DateTime dt = Now ();
|
||||
|
||||
while (Peek ())
|
||||
{
|
||||
foreach (TInputRecord r in Read ())
|
||||
{
|
||||
_inputQueue.Enqueue (r);
|
||||
}
|
||||
}
|
||||
|
||||
effectiveToken.ThrowIfCancellationRequested ();
|
||||
}
|
||||
while (!effectiveToken.IsCancellationRequested);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
Logging.Trace($"Stopping input processing");
|
||||
linkedCts?.Dispose ();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When implemented in a derived class, returns true if there is data available
|
||||
/// to read from console.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract bool Peek ();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the available data without blocking, called when <see cref="Peek"/>
|
||||
/// returns <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract IEnumerable<TInputRecord> Read ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Dispose () { }
|
||||
}
|
||||
@@ -5,30 +5,67 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>.
|
||||
/// Processes the queued input queue contents - which must be of Type <typeparamref name="TInputRecord"/>.
|
||||
/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
|
||||
/// events and data models.
|
||||
/// events and data models. Runs on the main loop thread.
|
||||
/// </summary>
|
||||
public abstract class InputProcessor<T> : IInputProcessor
|
||||
public abstract class InputProcessorImpl<TInputRecord> : IInputProcessor, IDisposable where TInputRecord : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs base instance including wiring all relevant
|
||||
/// parser events and setting <see cref="InputQueue"/> to
|
||||
/// the provided thread safe input collection.
|
||||
/// </summary>
|
||||
/// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IInput{T}"/>)</param>
|
||||
/// <param name="keyConverter">
|
||||
/// Key converter for translating driver specific
|
||||
/// <typeparamref name="TInputRecord"/> class into Terminal.Gui <see cref="Key"/>.
|
||||
/// </param>
|
||||
protected InputProcessorImpl (ConcurrentQueue<TInputRecord> inputBuffer, IKeyConverter<TInputRecord> keyConverter)
|
||||
{
|
||||
InputQueue = inputBuffer;
|
||||
Parser.HandleMouse = true;
|
||||
Parser.Mouse += (s, e) => RaiseMouseEvent (e);
|
||||
|
||||
Parser.HandleKeyboard = true;
|
||||
|
||||
Parser.Keyboard += (s, k) =>
|
||||
{
|
||||
RaiseKeyDownEvent (k);
|
||||
RaiseKeyUpEvent (k);
|
||||
};
|
||||
|
||||
// TODO: For now handle all other escape codes with ignore
|
||||
Parser.UnexpectedResponseHandler = str =>
|
||||
{
|
||||
var cur = new string (str.Select (k => k.Item1).ToArray ());
|
||||
Logging.Logger.LogInformation ($"{nameof (InputProcessorImpl<TInputRecord>)} ignored unrecognized response '{cur}'");
|
||||
AnsiSequenceSwallowed?.Invoke (this, cur);
|
||||
|
||||
return true;
|
||||
};
|
||||
KeyConverter = keyConverter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
|
||||
/// </summary>
|
||||
private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
|
||||
|
||||
internal AnsiResponseParser<T> Parser { get; } = new ();
|
||||
internal AnsiResponseParser<TInputRecord> Parser { get; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g.
|
||||
/// Class responsible for translating the driver specific native input class <typeparamref name="TInputRecord"/> e.g.
|
||||
/// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
|
||||
/// internal library representations of Keys).
|
||||
/// </summary>
|
||||
public IKeyConverter<T> KeyConverter { get; }
|
||||
public IKeyConverter<TInputRecord> KeyConverter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Input buffer which will be drained from by this class.
|
||||
/// The input queue which is filled by <see cref="IInput{TInputRecord}"/> implementations running on the input thread.
|
||||
/// Implementations of this class should dequeue from this queue in <see cref="ProcessQueue"/> on the main loop thread.
|
||||
/// </summary>
|
||||
public ConcurrentQueue<T> InputBuffer { get; }
|
||||
public ConcurrentQueue<TInputRecord> InputQueue { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? DriverName { get; init; }
|
||||
@@ -38,110 +75,95 @@ public abstract class InputProcessor<T> : IInputProcessor
|
||||
|
||||
private readonly MouseInterpreter _mouseInterpreter = new ();
|
||||
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<Key>? KeyDown;
|
||||
|
||||
/// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<string>? AnsiSequenceSwallowed;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
|
||||
/// <see cref="OnKeyUp"/>.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyDown (Key a)
|
||||
/// <inheritdoc />
|
||||
public void RaiseKeyDownEvent (Key a)
|
||||
{
|
||||
Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}");
|
||||
KeyDown?.Invoke (this, a);
|
||||
}
|
||||
|
||||
/// <summary>Event fired when a key is released.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
|
||||
/// complete.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<Key>? KeyUp;
|
||||
|
||||
/// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
|
||||
/// <remarks>
|
||||
/// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
|
||||
/// is complete.
|
||||
/// </remarks>
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
|
||||
/// <inheritdoc />
|
||||
public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); }
|
||||
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IInput<TInputRecord>? InputImpl { get; set; } // Set by MainLoopCoordinator
|
||||
|
||||
/// <inheritdoc />
|
||||
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<TInputRecord> testableInput)
|
||||
{
|
||||
testableInput.AddInput (inputRecord);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnqueueKeyUpEvent (Key key)
|
||||
{
|
||||
// TODO: Determine if we can still support this on Windows
|
||||
throw new NotImplementedException ();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnMouseEvent (MouseEventArgs a)
|
||||
/// <inheritdoc />
|
||||
public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent)
|
||||
{
|
||||
// Base implementation: For drivers where TInputRecord cannot represent mouse events
|
||||
// (e.g., ConsoleKeyInfo), derived classes should override this method.
|
||||
// See WindowsInputProcessor for an example implementation that converts MouseEventArgs
|
||||
// to InputRecord and enqueues it.
|
||||
Logging.Logger.LogWarning (
|
||||
$"{DriverName ?? "Unknown"} driver's InputProcessor does not support EnqueueMouseEvent. " +
|
||||
"Override this method to enable mouse event enqueueing for testing.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs base instance including wiring all relevant
|
||||
/// parser events and setting <see cref="InputBuffer"/> to
|
||||
/// the provided thread safe input collection.
|
||||
/// </summary>
|
||||
/// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param>
|
||||
/// <param name="keyConverter">
|
||||
/// Key converter for translating driver specific
|
||||
/// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.
|
||||
/// </param>
|
||||
protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
|
||||
{
|
||||
InputBuffer = inputBuffer;
|
||||
Parser.HandleMouse = true;
|
||||
Parser.Mouse += (s, e) => OnMouseEvent (e);
|
||||
|
||||
Parser.HandleKeyboard = true;
|
||||
|
||||
Parser.Keyboard += (s, k) =>
|
||||
{
|
||||
OnKeyDown (k);
|
||||
OnKeyUp (k);
|
||||
};
|
||||
|
||||
// TODO: For now handle all other escape codes with ignore
|
||||
Parser.UnexpectedResponseHandler = str =>
|
||||
{
|
||||
var cur = new string (str.Select (k => k.Item1).ToArray ());
|
||||
Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'");
|
||||
AnsiSequenceSwallowed?.Invoke (this, cur);
|
||||
|
||||
return true;
|
||||
};
|
||||
KeyConverter = keyConverter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void ProcessQueue ()
|
||||
{
|
||||
while (InputBuffer.TryDequeue (out T? input))
|
||||
while (InputQueue.TryDequeue (out TInputRecord input))
|
||||
{
|
||||
Process (input);
|
||||
}
|
||||
|
||||
foreach (T input in ReleaseParserHeldKeysIfStale ())
|
||||
foreach (TInputRecord input in ReleaseParserHeldKeysIfStale ())
|
||||
{
|
||||
ProcessAfterParsing (input);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
|
||||
private IEnumerable<TInputRecord> ReleaseParserHeldKeysIfStale ()
|
||||
{
|
||||
if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
|
||||
&& DateTime.Now - Parser.StateChangedAt > _escTimeout)
|
||||
@@ -154,17 +176,27 @@ public abstract class InputProcessor<T> : IInputProcessor
|
||||
|
||||
/// <summary>
|
||||
/// Process the provided single input element <paramref name="input"/>. This method
|
||||
/// is called sequentially for each value read from <see cref="InputBuffer"/>.
|
||||
/// is called sequentially for each value read from <see cref="InputQueue"/>.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
protected abstract void Process (T input);
|
||||
protected abstract void Process (TInputRecord input);
|
||||
|
||||
/// <summary>
|
||||
/// Process the provided single input element - short-circuiting the <see cref="Parser"/>
|
||||
/// stage of the processing.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
protected abstract void ProcessAfterParsing (T input);
|
||||
protected virtual void ProcessAfterParsing (TInputRecord input)
|
||||
{
|
||||
var key = KeyConverter.ToKey (input);
|
||||
|
||||
// If the key is not valid, we don't want to raise any events.
|
||||
if (IsValidInput (key, out key))
|
||||
{
|
||||
RaiseKeyDownEvent (key);
|
||||
RaiseKeyUpEvent (key);
|
||||
}
|
||||
}
|
||||
|
||||
private char _highSurrogate = '\0';
|
||||
|
||||
@@ -221,4 +253,10 @@ public abstract class InputProcessor<T> : IInputProcessor
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose () { ExternalCancellationTokenSource?.Dispose (); }
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IConsoleDriver"/>s and provides a
|
||||
/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IDriver"/>s and provides a
|
||||
/// consistent way for application code to specify keys and receive key events.
|
||||
/// <para>
|
||||
/// The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class to assist with implementing <see cref="IConsoleOutput"/>.
|
||||
/// Abstract base class to assist with implementing <see cref="IOutput"/>.
|
||||
/// </summary>
|
||||
public abstract class OutputBase
|
||||
{
|
||||
@@ -18,14 +18,9 @@ public abstract class OutputBase
|
||||
/// <param name="visibility"></param>
|
||||
public abstract void SetCursorVisibility (CursorVisibility visibility);
|
||||
|
||||
/// <inheritdoc cref="IConsoleOutput.Write(IOutputBuffer)"/>
|
||||
/// <inheritdoc cref="IOutput.Write(IOutputBuffer)"/>
|
||||
public virtual void Write (IOutputBuffer buffer)
|
||||
{
|
||||
if (ConsoleDriver.RunningUnitTests)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
int rows = buffer.Rows;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Terminal.Gui.Drivers;
|
||||
/// draw operations before being flushed to the console as part of the main loop.
|
||||
/// operation
|
||||
/// </summary>
|
||||
public class OutputBuffer : IOutputBuffer
|
||||
public class OutputBufferImpl : IOutputBuffer
|
||||
{
|
||||
/// <summary>
|
||||
/// The contents of the application output. The driver outputs this buffer to the terminal when
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user