Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop

This commit is contained in:
Tig
2025-11-11 18:39:54 -07:00
310 changed files with 14827 additions and 16911 deletions

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
View File

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

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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!);

View File

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

View File

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

View File

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

View File

@@ -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) });

View File

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

View File

@@ -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

View File

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

View File

@@ -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}";
}
}

View File

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

View File

@@ -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);

View File

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

View File

@@ -1,6 +1,6 @@
![.NET Core](https://github.com/gui-cs/Terminal.Gui/workflows/.NET%20Core/badge.svg?branch=develop)
[![Version](https://img.shields.io/nuget/v/Terminal.Gui.svg)](https://www.nuget.org/packages/Terminal.Gui)
![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27/raw/code-coverage.json)
[![codecov](https://codecov.io/gh/gui-cs/Terminal.Gui/branch/v2_develop/graph/badge.svg)](https://codecov.io/gh/gui-cs/Terminal.Gui)
[![Downloads](https://img.shields.io/nuget/dt/Terminal.Gui)](https://www.nuget.org/packages/Terminal.Gui)
[![License](https://img.shields.io/github/license/gui-cs/gui.cs.svg)](LICENSE)
![Bugs](https://img.shields.io/github/issues/gui-cs/gui.cs/bug)

View 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"

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">

View File

@@ -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);
}
}

View 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); }
}

View 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); }
}

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

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

View File

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

View File

@@ -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>

View File

@@ -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
{

View File

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

View File

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

View File

@@ -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"/>.

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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
{

View File

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

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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 (

View File

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

View File

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

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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 ()}),";
}
}

View File

@@ -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); }
}

View File

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

View File

@@ -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))

View File

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

View File

@@ -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

View File

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

View File

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

View 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);
}
}

View 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

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

View File

@@ -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);
}

View File

@@ -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>

View 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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

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

View File

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

View 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);
}

View 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 () { }
}

View File

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

View File

@@ -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

View File

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

View File

@@ -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