Upgrade solution to C# 10 and .NET 6
This commit is contained in:
Родитель
5ba38e544d
Коммит
10cef58def
|
@ -30,6 +30,9 @@ trim_trailing_whitespace = false
|
|||
end_of_line = lf
|
||||
|
||||
[*.cs]
|
||||
# Prefer file scoped namespace declarations
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_separate_import_directive_groups = false
|
||||
|
|
|
@ -39,8 +39,6 @@ jobs:
|
|||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 5.0.301
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
|
|
|
@ -49,8 +49,6 @@ jobs:
|
|||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 5.0.301
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
|
@ -79,8 +77,6 @@ jobs:
|
|||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 5.0.301
|
||||
|
||||
- name: Publish
|
||||
shell: bash
|
||||
|
|
10
build.cake
10
build.cake
|
@ -7,7 +7,7 @@ var configuration = Argument("configuration", "Release");
|
|||
Task("Build")
|
||||
.Does(context =>
|
||||
{
|
||||
DotNetCoreBuild("./src/Spectre.Terminals.sln", new DotNetCoreBuildSettings {
|
||||
DotNetBuild("./src/Spectre.Terminals.sln", new DotNetBuildSettings {
|
||||
Configuration = configuration,
|
||||
NoIncremental = context.HasArgument("rebuild"),
|
||||
MSBuildSettings = new DotNetCoreMSBuildSettings()
|
||||
|
@ -19,7 +19,7 @@ Task("Test")
|
|||
.IsDependentOn("Build")
|
||||
.Does(context =>
|
||||
{
|
||||
DotNetCoreTest("./src/Spectre.Terminals.Tests/Spectre.Terminals.Tests.csproj", new DotNetCoreTestSettings {
|
||||
DotNetTest("./src/Spectre.Terminals.Tests/Spectre.Terminals.Tests.csproj", new DotNetTestSettings {
|
||||
Configuration = configuration,
|
||||
NoRestore = true,
|
||||
NoBuild = true,
|
||||
|
@ -32,12 +32,12 @@ Task("Package")
|
|||
{
|
||||
context.CleanDirectory("./.artifacts");
|
||||
|
||||
context.DotNetCorePack($"./src/Spectre.Terminals.sln", new DotNetCorePackSettings {
|
||||
context.DotNetPack($"./src/Spectre.Terminals.sln", new DotNetPackSettings {
|
||||
Configuration = configuration,
|
||||
NoRestore = true,
|
||||
NoBuild = true,
|
||||
OutputDirectory = "./.artifacts",
|
||||
MSBuildSettings = new DotNetCoreMSBuildSettings()
|
||||
MSBuildSettings = new DotNetMSBuildSettings()
|
||||
.TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error)
|
||||
});
|
||||
});
|
||||
|
@ -56,7 +56,7 @@ Task("Publish-NuGet")
|
|||
foreach(var file in context.GetFiles("./.artifacts/*.nupkg"))
|
||||
{
|
||||
context.Information("Publishing {0}...", file.GetFilename().FullPath);
|
||||
DotNetCoreNuGetPush(file.FullPath, new DotNetCoreNuGetPushSettings
|
||||
DotNetNuGetPush(file.FullPath, new DotNetNuGetPushSettings
|
||||
{
|
||||
Source = "https://api.nuget.org/v3/index.json",
|
||||
ApiKey = apiKey,
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"cake.tool": {
|
||||
"version": "1.1.0",
|
||||
"version": "2.2.0",
|
||||
"commands": [
|
||||
"dotnet-cake"
|
||||
]
|
||||
},
|
||||
"dotnet-example": {
|
||||
"version": "1.4.0",
|
||||
"version": "1.6.0",
|
||||
"commands": [
|
||||
"dotnet-example"
|
||||
]
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ExampleTitle>Ansi</ExampleTitle>
|
||||
<ExampleDescription>Demonstrates how to write VT/ANSI codes to the terminal</ExampleDescription>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,40 +1,39 @@
|
|||
using Spectre.Terminals;
|
||||
|
||||
namespace Examples
|
||||
namespace Examples;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static class Program
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var terminal = Terminal.Shared;
|
||||
var terminal = Terminal.Shared;
|
||||
|
||||
// Information
|
||||
terminal.WriteLine("\u001b[2J\u001b[1;1HSpectre.Terminal");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine($" Terminal driver = {terminal.Name}");
|
||||
terminal.WriteLine($" Window size = {terminal.Size}");
|
||||
terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}");
|
||||
terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}");
|
||||
terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}");
|
||||
terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("Press ANY key");
|
||||
terminal.WriteLine();
|
||||
// Information
|
||||
terminal.WriteLine("\u001b[2J\u001b[1;1HSpectre.Terminal");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine($" Terminal driver = {terminal.Name}");
|
||||
terminal.WriteLine($" Window size = {terminal.Size}");
|
||||
terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}");
|
||||
terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}");
|
||||
terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}");
|
||||
terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("Press ANY key");
|
||||
terminal.WriteLine();
|
||||
|
||||
// Do some line manipulation
|
||||
terminal.Write("\u001b[6;8H[Delete after]\u001b[0K");
|
||||
terminal.Write("\u001b[5;15H\u001b[1K[Delete before]");
|
||||
terminal.Write("\u001b[4;15H\u001b[2K[Delete line]");
|
||||
// Do some line manipulation
|
||||
terminal.Write("\u001b[6;8H[Delete after]\u001b[0K");
|
||||
terminal.Write("\u001b[5;15H\u001b[1K[Delete before]");
|
||||
terminal.Write("\u001b[4;15H\u001b[2K[Delete line]");
|
||||
|
||||
// Write some text in an alternate buffer
|
||||
terminal.Write("\u001b[?1049h");
|
||||
terminal.WriteLine("HELLO WORLD!");
|
||||
terminal.Write("\u001b[?1049l");
|
||||
terminal.Write("");
|
||||
// Write some text in an alternate buffer
|
||||
terminal.Write("\u001b[?1049h");
|
||||
terminal.WriteLine("HELLO WORLD!");
|
||||
terminal.Write("\u001b[?1049l");
|
||||
terminal.Write("");
|
||||
|
||||
terminal.Output.WriteLine("Goodbye!");
|
||||
}
|
||||
terminal.Output.WriteLine("Goodbye!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ExampleTitle>Info</ExampleTitle>
|
||||
<ExampleDescription>Shows terminal capabilities</ExampleDescription>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,69 +1,68 @@
|
|||
using Spectre.Terminals;
|
||||
|
||||
namespace Examples
|
||||
namespace Examples;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static class Program
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
var terminal = Terminal.Shared;
|
||||
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("\u001b[38;5;15m\u001b[48;5;9mSpectre.Terminals\u001b[0m");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine($" Terminal driver = {terminal.Name}");
|
||||
terminal.WriteLine($" Window size = {terminal.Size}");
|
||||
terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}");
|
||||
terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}");
|
||||
terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}");
|
||||
terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}");
|
||||
terminal.WriteLine();
|
||||
|
||||
WriteColors(terminal);
|
||||
}
|
||||
|
||||
private static void WriteColors(ITerminal terminal)
|
||||
{
|
||||
terminal.WriteLine("Spectre.Terminals");
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
var terminal = Terminal.Shared;
|
||||
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("\u001b[38;5;15m\u001b[48;5;9mSpectre.Terminals\u001b[0m");
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine($" Terminal driver = {terminal.Name}");
|
||||
terminal.WriteLine($" Window size = {terminal.Size}");
|
||||
terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}");
|
||||
terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}");
|
||||
terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}");
|
||||
terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}");
|
||||
terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}");
|
||||
terminal.WriteLine();
|
||||
|
||||
WriteColors(terminal);
|
||||
terminal.Write($"\u001b[38;5;{i}m\u001b[48;5;{i}m \u001b[0m");
|
||||
}
|
||||
|
||||
private static void WriteColors(ITerminal terminal)
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("System.Console");
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
terminal.WriteLine("Spectre.Terminals");
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
terminal.Write($"\u001b[38;5;{i}m\u001b[48;5;{i}m \u001b[0m");
|
||||
}
|
||||
|
||||
terminal.WriteLine();
|
||||
terminal.WriteLine("System.Console");
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
System.Console.BackgroundColor = GetColor(i);
|
||||
System.Console.Write(" ");
|
||||
}
|
||||
System.Console.ResetColor();
|
||||
System.Console.BackgroundColor = GetColor(i);
|
||||
System.Console.Write(" ");
|
||||
}
|
||||
System.Console.ResetColor();
|
||||
}
|
||||
|
||||
private static System.ConsoleColor GetColor(int number)
|
||||
private static System.ConsoleColor GetColor(int number)
|
||||
{
|
||||
return number switch
|
||||
{
|
||||
return number switch
|
||||
{
|
||||
0 => System.ConsoleColor.Black, // 0
|
||||
1 => System.ConsoleColor.DarkRed, // 4
|
||||
2 => System.ConsoleColor.DarkGreen, // 2
|
||||
3 => System.ConsoleColor.DarkYellow, // 6
|
||||
4 => System.ConsoleColor.DarkBlue, // 1
|
||||
5 => System.ConsoleColor.DarkMagenta, // 5
|
||||
6 => System.ConsoleColor.DarkCyan, // 3
|
||||
7 => System.ConsoleColor.Gray, // 7
|
||||
8 => System.ConsoleColor.DarkGray, // 8
|
||||
9 => System.ConsoleColor.Red, // 12
|
||||
10 => System.ConsoleColor.Green, // 10
|
||||
11 => System.ConsoleColor.Yellow, // 14
|
||||
12 => System.ConsoleColor.Blue, // 9
|
||||
13 => System.ConsoleColor.Magenta, // 13
|
||||
14 => System.ConsoleColor.Cyan, // 11
|
||||
15 => System.ConsoleColor.White, // 15
|
||||
_ => throw new System.InvalidOperationException("Cannot convert color to console color."),
|
||||
};
|
||||
}
|
||||
0 => System.ConsoleColor.Black, // 0
|
||||
1 => System.ConsoleColor.DarkRed, // 4
|
||||
2 => System.ConsoleColor.DarkGreen, // 2
|
||||
3 => System.ConsoleColor.DarkYellow, // 6
|
||||
4 => System.ConsoleColor.DarkBlue, // 1
|
||||
5 => System.ConsoleColor.DarkMagenta, // 5
|
||||
6 => System.ConsoleColor.DarkCyan, // 3
|
||||
7 => System.ConsoleColor.Gray, // 7
|
||||
8 => System.ConsoleColor.DarkGray, // 8
|
||||
9 => System.ConsoleColor.Red, // 12
|
||||
10 => System.ConsoleColor.Green, // 10
|
||||
11 => System.ConsoleColor.Yellow, // 14
|
||||
12 => System.ConsoleColor.Blue, // 9
|
||||
13 => System.ConsoleColor.Magenta, // 13
|
||||
14 => System.ConsoleColor.Cyan, // 11
|
||||
15 => System.ConsoleColor.White, // 15
|
||||
_ => throw new System.InvalidOperationException("Cannot convert color to console color."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ExampleTitle>Input</ExampleTitle>
|
||||
<ExampleDescription>Demonstrates how to receive input from STDIN</ExampleDescription>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
using System;
|
||||
using Spectre.Terminals;
|
||||
|
||||
namespace Input
|
||||
namespace Examples;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static class Program
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ReadLine();
|
||||
ReadKeys();
|
||||
}
|
||||
ReadLine();
|
||||
ReadKeys();
|
||||
}
|
||||
|
||||
private static void ReadLine()
|
||||
{
|
||||
Terminal.Shared.Write("Write something> ");
|
||||
var line = Terminal.Shared.Input.ReadLine();
|
||||
Terminal.Shared.WriteLine($"Read = {line}");
|
||||
}
|
||||
private static void ReadLine()
|
||||
{
|
||||
Terminal.Shared.Write("Write something> ");
|
||||
var line = Terminal.Shared.Input.ReadLine();
|
||||
Terminal.Shared.WriteLine($"Read = {line}");
|
||||
}
|
||||
|
||||
private static void ReadKeys()
|
||||
{
|
||||
Terminal.Shared.WriteLine();
|
||||
Terminal.Shared.WriteLine("[Press any keys]");
|
||||
private static void ReadKeys()
|
||||
{
|
||||
Terminal.Shared.WriteLine();
|
||||
Terminal.Shared.WriteLine("[Press any keys]");
|
||||
|
||||
while (true)
|
||||
while (true)
|
||||
{
|
||||
// Read a key from the keyboard
|
||||
var key = Terminal.Shared.Input.ReadKey();
|
||||
if (key.Key == ConsoleKey.Escape)
|
||||
{
|
||||
// Read a key from the keyboard
|
||||
var key = Terminal.Shared.Input.ReadKey();
|
||||
if (key.Key == ConsoleKey.Escape)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the character representation
|
||||
var character = !char.IsWhiteSpace(key.KeyChar)
|
||||
? key.KeyChar : '*';
|
||||
|
||||
// Write to terminal
|
||||
Terminal.Shared.WriteLine(
|
||||
$"{character} [KEY={key.Key} MOD={key.Modifiers}]");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the character representation
|
||||
var character = !char.IsWhiteSpace(key.KeyChar)
|
||||
? key.KeyChar : '*';
|
||||
|
||||
// Write to terminal
|
||||
Terminal.Shared.WriteLine(
|
||||
$"{character} [KEY={key.Key} MOD={key.Modifiers}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
using System.Threading;
|
||||
using Spectre.Terminals;
|
||||
|
||||
namespace Examples
|
||||
namespace Examples;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static class Program
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
var terminal = Terminal.Shared;
|
||||
var cancel = new ManualResetEvent(false);
|
||||
|
||||
// Hook up signal handling
|
||||
terminal.Signalled += (s, e) =>
|
||||
{
|
||||
var terminal = Terminal.Shared;
|
||||
var cancel = new ManualResetEvent(false);
|
||||
|
||||
// Hook up signal handling
|
||||
terminal.Signalled += (s, e) =>
|
||||
if (e.Signal == TerminalSignal.SIGINT)
|
||||
{
|
||||
if (e.Signal == TerminalSignal.SIGINT)
|
||||
{
|
||||
terminal.WriteLine("Received \u001b[38;5;14mSIGINT\u001b[0m");
|
||||
e.Cancel = true;
|
||||
cancel.Set();
|
||||
}
|
||||
else if(e.Signal == TerminalSignal.SIGQUIT)
|
||||
{
|
||||
terminal.WriteLine("Received \u001b[38;5;14mSIGQUIT\u001b[0m");
|
||||
e.Cancel = true;
|
||||
cancel.Set();
|
||||
}
|
||||
};
|
||||
terminal.WriteLine("Received \u001b[38;5;14mSIGINT\u001b[0m");
|
||||
e.Cancel = true;
|
||||
cancel.Set();
|
||||
}
|
||||
else if(e.Signal == TerminalSignal.SIGQUIT)
|
||||
{
|
||||
terminal.WriteLine("Received \u001b[38;5;14mSIGQUIT\u001b[0m");
|
||||
e.Cancel = true;
|
||||
cancel.Set();
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for a signal
|
||||
terminal.WriteLine("Press CTRL+C or CTRL+BREAK to quit");
|
||||
cancel.WaitOne();
|
||||
terminal.WriteLine("Bye!");
|
||||
}
|
||||
// Wait for a signal
|
||||
terminal.WriteLine("Press CTRL+C or CTRL+BREAK to quit");
|
||||
cancel.WaitOne();
|
||||
terminal.WriteLine("Bye!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ExampleTitle>Signals</ExampleTitle>
|
||||
<ExampleDescription>Demonstrates how to listen for signals</ExampleDescription>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"projects": [ "src" ],
|
||||
"sdk": {
|
||||
"version": "5.0.301",
|
||||
"rollForward": "latestPatch"
|
||||
"version": "6.0.300",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
<Project>
|
||||
<PropertyGroup Label="Settings">
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>embedded</DebugType>
|
||||
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(GITHUB_ACTIONS)' == 'true'">
|
||||
|
@ -31,16 +32,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="MinVer" PrivateAssets="All" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
|
||||
<PackageReference Include="MinVer" PrivateAssets="All" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.312">
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="3.0.0">
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.1.1">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,192 +1,186 @@
|
|||
using System.Text;
|
||||
using Shouldly;
|
||||
using Spectre.Terminals.Emulation;
|
||||
using Xunit;
|
||||
namespace Spectre.Terminals.Tests;
|
||||
|
||||
namespace Spectre.Terminals.Tests
|
||||
public sealed class AnsiSequenceTests
|
||||
{
|
||||
public sealed class AnsiSequenceTests
|
||||
public sealed class TheInterpretMethod
|
||||
{
|
||||
public sealed class TheInterpretMethod
|
||||
[Fact]
|
||||
public void Should_Interpret_Codes_As_Expected()
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Interpret_Codes_As_Expected()
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(
|
||||
printer, state,
|
||||
"\u001b[?1049h " +
|
||||
"\u001b[?25l\u001b[2KHello World!\u001b[?25h " +
|
||||
"\u001b[3A \u001b[4B \u001b[5C \u001b[6D " +
|
||||
"\u001b[7E \u001b[8F \u001b[9G \u001b[10;11H " +
|
||||
"\u001b[0J \u001b[1J \u001b[2J \u001b[3J " +
|
||||
"\u001b[0K \u001b[1K \u001b[2K " +
|
||||
"\u001b[s \u001b[u " +
|
||||
"\u001b[?1049l");
|
||||
|
||||
// Then
|
||||
state.ToString()
|
||||
.ShouldBe(
|
||||
"[EnableAltBuffer] " +
|
||||
"[HideCursor][EL2]Hello World![ShowCursor] " +
|
||||
"[CUU3] [CUD4] [CUF5] [CUB6] " +
|
||||
"[CNL7] [CPL8] [CHA9] [CUP10,11] " +
|
||||
"[ED0] [ED1] [ED2] [ED3] " +
|
||||
"[EL0] [EL1] [EL2] " +
|
||||
"[SaveCursor] [RestoreCursor] " +
|
||||
"[DisableAltBuffer]");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\u001b[35m", "[SGR-FG=5]")]
|
||||
[InlineData("\u001b[38;5;29m", "[SGR-FG=29]")]
|
||||
[InlineData("\u001b[38;2;92;128;255m", "[SGR-FG=92,128,255]")]
|
||||
[InlineData("\u001b[42m", "[SGR-BG=2]")]
|
||||
[InlineData("\u001b[48;5;29m", "[SGR-BG=29]")]
|
||||
[InlineData("\u001b[48;2;92;128;255m", "[SGR-BG=92,128,255]")]
|
||||
[InlineData("\u001b[0m", "[SGR-RESET]")]
|
||||
public void Should_Interpret_SGR_Attributes_Correctly(string input, string expected)
|
||||
{
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(printer, state, input);
|
||||
|
||||
// Then
|
||||
state.ToString().ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\u001b[38;5m")]
|
||||
[InlineData("\u001b[38;2;92;128m")]
|
||||
[InlineData("\u001b[38;2;92m")]
|
||||
[InlineData("\u001b[38;2m")]
|
||||
[InlineData("\u001b[48;5m")]
|
||||
[InlineData("\u001b[48;2;92;128m")]
|
||||
[InlineData("\u001b[48;2;92m")]
|
||||
[InlineData("\u001b[48;2m")]
|
||||
public void Should_Not_Parse_Malformed_SGR_Attributes(string input)
|
||||
{
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(printer, state, input);
|
||||
|
||||
// Then
|
||||
state.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
private sealed class AnsiPrinter : AnsiSequenceVisitor<StringBuilder>
|
||||
{
|
||||
protected override void PrintText(PrintText instruction, StringBuilder state)
|
||||
{
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(
|
||||
printer, state,
|
||||
"\u001b[?1049h " +
|
||||
"\u001b[?25l\u001b[2KHello World!\u001b[?25h " +
|
||||
"\u001b[3A \u001b[4B \u001b[5C \u001b[6D " +
|
||||
"\u001b[7E \u001b[8F \u001b[9G \u001b[10;11H " +
|
||||
"\u001b[0J \u001b[1J \u001b[2J \u001b[3J " +
|
||||
"\u001b[0K \u001b[1K \u001b[2K " +
|
||||
"\u001b[s \u001b[u " +
|
||||
"\u001b[?1049l");
|
||||
|
||||
// Then
|
||||
state.ToString()
|
||||
.ShouldBe(
|
||||
"[EnableAltBuffer] " +
|
||||
"[HideCursor][EL2]Hello World![ShowCursor] " +
|
||||
"[CUU3] [CUD4] [CUF5] [CUB6] " +
|
||||
"[CNL7] [CPL8] [CHA9] [CUP10,11] " +
|
||||
"[ED0] [ED1] [ED2] [ED3] " +
|
||||
"[EL0] [EL1] [EL2] " +
|
||||
"[SaveCursor] [RestoreCursor] " +
|
||||
"[DisableAltBuffer]");
|
||||
state.Append(instruction.Text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\u001b[35m", "[SGR-FG=5]")]
|
||||
[InlineData("\u001b[38;5;29m", "[SGR-FG=29]")]
|
||||
[InlineData("\u001b[38;2;92;128;255m", "[SGR-FG=92,128,255]")]
|
||||
[InlineData("\u001b[42m", "[SGR-BG=2]")]
|
||||
[InlineData("\u001b[48;5;29m", "[SGR-BG=29]")]
|
||||
[InlineData("\u001b[48;2;92;128;255m", "[SGR-BG=92,128,255]")]
|
||||
[InlineData("\u001b[0m", "[SGR-RESET]")]
|
||||
public void Should_Interpret_SGR_Attributes_Correctly(string input, string expected)
|
||||
protected override void ShowCursor(ShowCursor instruction, StringBuilder state)
|
||||
{
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(printer, state, input);
|
||||
|
||||
// Then
|
||||
state.ToString().ShouldBe(expected);
|
||||
state.Append("[ShowCursor]");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\u001b[38;5m")]
|
||||
[InlineData("\u001b[38;2;92;128m")]
|
||||
[InlineData("\u001b[38;2;92m")]
|
||||
[InlineData("\u001b[38;2m")]
|
||||
[InlineData("\u001b[48;5m")]
|
||||
[InlineData("\u001b[48;2;92;128m")]
|
||||
[InlineData("\u001b[48;2;92m")]
|
||||
[InlineData("\u001b[48;2m")]
|
||||
public void Should_Not_Parse_Malformed_SGR_Attributes(string input)
|
||||
protected override void HideCursor(HideCursor instruction, StringBuilder state)
|
||||
{
|
||||
// Given
|
||||
var printer = new AnsiPrinter();
|
||||
var state = new StringBuilder();
|
||||
|
||||
// When
|
||||
AnsiInterpreter.Interpret(printer, state, input);
|
||||
|
||||
// Then
|
||||
state.Length.ShouldBe(0);
|
||||
state.Append("[HideCursor]");
|
||||
}
|
||||
|
||||
private sealed class AnsiPrinter : AnsiSequenceVisitor<StringBuilder>
|
||||
protected override void CursorUp(CursorUp instruction, StringBuilder state)
|
||||
{
|
||||
protected override void PrintText(PrintText instruction, StringBuilder state)
|
||||
{
|
||||
state.Append(instruction.Text);
|
||||
}
|
||||
state.Append("[CUU").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void ShowCursor(ShowCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[ShowCursor]");
|
||||
}
|
||||
protected override void CursorDown(CursorDown instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUD").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void HideCursor(HideCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[HideCursor]");
|
||||
}
|
||||
protected override void CursorBack(CursorBack instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUB").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorUp(CursorUp instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUU").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void CursorForward(CursorForward instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUF").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorDown(CursorDown instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUD").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CHA").Append(instruction.Column).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorBack(CursorBack instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUB").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void CursorNextLine(CursorNextLine instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CNL").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorForward(CursorForward instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUF").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void CursorPosition(CursorPosition instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUP").Append(instruction.Row).Append(',').Append(instruction.Column).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CHA").Append(instruction.Column).Append(']');
|
||||
}
|
||||
protected override void CursorPreviousLine(CursorPreviousLine instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CPL").Append(instruction.Count).Append(']');
|
||||
}
|
||||
|
||||
protected override void CursorNextLine(CursorNextLine instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CNL").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[DisableAltBuffer]");
|
||||
}
|
||||
|
||||
protected override void CursorPosition(CursorPosition instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CUP").Append(instruction.Row).Append(',').Append(instruction.Column).Append(']');
|
||||
}
|
||||
protected override void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[EnableAltBuffer]");
|
||||
}
|
||||
|
||||
protected override void CursorPreviousLine(CursorPreviousLine instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[CPL").Append(instruction.Count).Append(']');
|
||||
}
|
||||
protected override void EraseInDisplay(EraseInDisplay instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[ED").Append(instruction.Mode).Append(']');
|
||||
}
|
||||
|
||||
protected override void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[DisableAltBuffer]");
|
||||
}
|
||||
protected override void EraseInLine(EraseInLine instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[EL").Append(instruction.Mode).Append(']');
|
||||
}
|
||||
|
||||
protected override void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[EnableAltBuffer]");
|
||||
}
|
||||
protected override void RestoreCursor(RestoreCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[RestoreCursor]");
|
||||
}
|
||||
|
||||
protected override void EraseInDisplay(EraseInDisplay instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[ED").Append(instruction.Mode).Append(']');
|
||||
}
|
||||
protected override void StoreCursor(StoreCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[SaveCursor]");
|
||||
}
|
||||
|
||||
protected override void EraseInLine(EraseInLine instruction, StringBuilder state)
|
||||
protected override void SelectGraphicRendition(SelectGraphicRendition instruction, StringBuilder state)
|
||||
{
|
||||
foreach (var operation in instruction.Operations)
|
||||
{
|
||||
state.Append("[EL").Append(instruction.Mode).Append(']');
|
||||
}
|
||||
|
||||
protected override void RestoreCursor(RestoreCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[RestoreCursor]");
|
||||
}
|
||||
|
||||
protected override void StoreCursor(StoreCursor instruction, StringBuilder state)
|
||||
{
|
||||
state.Append("[SaveCursor]");
|
||||
}
|
||||
|
||||
protected override void SelectGraphicRendition(SelectGraphicRendition instruction, StringBuilder state)
|
||||
{
|
||||
foreach (var operation in instruction.Operations)
|
||||
if (operation.Reset)
|
||||
{
|
||||
if (operation.Reset)
|
||||
{
|
||||
state.Append("[SGR-RESET]");
|
||||
}
|
||||
else if (operation.Foreground != null)
|
||||
{
|
||||
state.Append($"[SGR-FG={operation.Foreground}]");
|
||||
}
|
||||
else if (operation.Background != null)
|
||||
{
|
||||
state.Append($"[SGR-BG={operation.Background}]");
|
||||
}
|
||||
state.Append("[SGR-RESET]");
|
||||
}
|
||||
else if (operation.Foreground != null)
|
||||
{
|
||||
state.Append($"[SGR-FG={operation.Foreground}]");
|
||||
}
|
||||
else if (operation.Background != null)
|
||||
{
|
||||
state.Append($"[SGR-BG={operation.Background}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
global using System.Text;
|
||||
global using Shouldly;
|
||||
global using Spectre.Terminals.Emulation;
|
||||
global using Xunit;
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -10,14 +10,14 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -1,98 +1,95 @@
|
|||
using Spectre.Terminals.Emulation;
|
||||
namespace Spectre.Terminals.Tests;
|
||||
|
||||
namespace Spectre.Terminals.Tests
|
||||
internal abstract class AnsiSequenceVisitor<TState> : IAnsiSequenceVisitor<TState>
|
||||
{
|
||||
internal abstract class AnsiSequenceVisitor<TState> : IAnsiSequenceVisitor<TState>
|
||||
void IAnsiSequenceVisitor<TState>.CursorBack(CursorBack instruction, TState state) => CursorBack(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorDown(CursorDown instruction, TState state) => CursorDown(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorForward(CursorForward instruction, TState state) => CursorForward(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state) => CursorHorizontalAbsolute(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorNextLine(CursorNextLine instruction, TState state) => CursorNextLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorPosition(CursorPosition instruction, TState state) => CursorPosition(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorPreviousLine(CursorPreviousLine instruction, TState state) => CursorPreviousLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorUp(CursorUp instruction, TState state) => CursorUp(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EraseInDisplay(EraseInDisplay instruction, TState state) => EraseInDisplay(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EraseInLine(EraseInLine instruction, TState state) => EraseInLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.PrintText(PrintText instruction, TState state) => PrintText(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.RestoreCursor(RestoreCursor instruction, TState state) => RestoreCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.StoreCursor(StoreCursor instruction, TState state) => StoreCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.HideCursor(HideCursor instruction, TState state) => HideCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.ShowCursor(ShowCursor instruction, TState state) => ShowCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state) => EnableAlternativeBuffer(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state) => DisableAlternativeBuffer(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.SelectGraphicRendition(SelectGraphicRendition instruction, TState state) => SelectGraphicRendition(instruction, state);
|
||||
|
||||
protected virtual void CursorBack(CursorBack instruction, TState state)
|
||||
{
|
||||
void IAnsiSequenceVisitor<TState>.CursorBack(CursorBack instruction, TState state) => CursorBack(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorDown(CursorDown instruction, TState state) => CursorDown(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorForward(CursorForward instruction, TState state) => CursorForward(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state) => CursorHorizontalAbsolute(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorNextLine(CursorNextLine instruction, TState state) => CursorNextLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorPosition(CursorPosition instruction, TState state) => CursorPosition(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorPreviousLine(CursorPreviousLine instruction, TState state) => CursorPreviousLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.CursorUp(CursorUp instruction, TState state) => CursorUp(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EraseInDisplay(EraseInDisplay instruction, TState state) => EraseInDisplay(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EraseInLine(EraseInLine instruction, TState state) => EraseInLine(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.PrintText(PrintText instruction, TState state) => PrintText(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.RestoreCursor(RestoreCursor instruction, TState state) => RestoreCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.StoreCursor(StoreCursor instruction, TState state) => StoreCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.HideCursor(HideCursor instruction, TState state) => HideCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.ShowCursor(ShowCursor instruction, TState state) => ShowCursor(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state) => EnableAlternativeBuffer(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state) => DisableAlternativeBuffer(instruction, state);
|
||||
void IAnsiSequenceVisitor<TState>.SelectGraphicRendition(SelectGraphicRendition instruction, TState state) => SelectGraphicRendition(instruction, state);
|
||||
}
|
||||
|
||||
protected virtual void CursorBack(CursorBack instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorDown(CursorDown instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorDown(CursorDown instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorForward(CursorForward instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorForward(CursorForward instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorNextLine(CursorNextLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorNextLine(CursorNextLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorPosition(CursorPosition instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorPosition(CursorPosition instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorPreviousLine(CursorPreviousLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorPreviousLine(CursorPreviousLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void CursorUp(CursorUp instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CursorUp(CursorUp instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void EraseInDisplay(EraseInDisplay instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void EraseInDisplay(EraseInDisplay instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void EraseInLine(EraseInLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void EraseInLine(EraseInLine instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void PrintText(PrintText instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void PrintText(PrintText instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void RestoreCursor(RestoreCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void RestoreCursor(RestoreCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void StoreCursor(StoreCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void StoreCursor(StoreCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void HideCursor(HideCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void HideCursor(HideCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void ShowCursor(ShowCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void ShowCursor(ShowCursor instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void SelectGraphicRendition(SelectGraphicRendition instruction, TState state)
|
||||
{
|
||||
}
|
||||
protected virtual void SelectGraphicRendition(SelectGraphicRendition instruction, TState state)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
namespace Spectre.Terminals
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents different ways of clearing a display.
|
||||
/// </summary>
|
||||
public enum ClearDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents different ways of clearing a display.
|
||||
/// Clears the whole display.
|
||||
/// </summary>
|
||||
public enum ClearDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the whole display.
|
||||
/// </summary>
|
||||
Everything = 0,
|
||||
Everything = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Clears the whole display, including the
|
||||
/// scrollback buffer.
|
||||
/// </summary>
|
||||
EverythingAndScrollbackBuffer = 1,
|
||||
/// <summary>
|
||||
/// Clears the whole display, including the
|
||||
/// scrollback buffer.
|
||||
/// </summary>
|
||||
EverythingAndScrollbackBuffer = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Clears everything before the cursor.
|
||||
/// </summary>
|
||||
BeforeCursor = 2,
|
||||
/// <summary>
|
||||
/// Clears everything before the cursor.
|
||||
/// </summary>
|
||||
BeforeCursor = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Clears everything after the cursor.
|
||||
/// </summary>
|
||||
AfterCursor = 3,
|
||||
}
|
||||
/// <summary>
|
||||
/// Clears everything after the cursor.
|
||||
/// </summary>
|
||||
AfterCursor = 3,
|
||||
}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
namespace Spectre.Terminals
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents different ways of clearing a line.
|
||||
/// </summary>
|
||||
public enum ClearLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents different ways of clearing a line.
|
||||
/// Clears the whole line.
|
||||
/// </summary>
|
||||
public enum ClearLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the whole line.
|
||||
/// </summary>
|
||||
WholeLine = 0,
|
||||
WholeLine = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Clears everything before the cursor.
|
||||
/// </summary>
|
||||
BeforeCursor = 1,
|
||||
/// <summary>
|
||||
/// Clears everything before the cursor.
|
||||
/// </summary>
|
||||
BeforeCursor = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Clears everything after the cursor.
|
||||
/// </summary>
|
||||
AfterCursor = 2,
|
||||
}
|
||||
/// <summary>
|
||||
/// Clears everything after the cursor.
|
||||
/// </summary>
|
||||
AfterCursor = 2,
|
||||
}
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
namespace Spectre.Terminals
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a cursor direction.
|
||||
/// </summary>
|
||||
public enum CursorDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a cursor direction.
|
||||
/// Moves the cursor forward.
|
||||
/// </summary>
|
||||
public enum CursorDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves the cursor forward.
|
||||
/// </summary>
|
||||
Forward = 0,
|
||||
Forward = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor backwards.
|
||||
/// </summary>
|
||||
Back = 1,
|
||||
/// <summary>
|
||||
/// Moves the cursor backwards.
|
||||
/// </summary>
|
||||
Back = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor up.
|
||||
/// </summary>
|
||||
Up = 2,
|
||||
/// <summary>
|
||||
/// Moves the cursor up.
|
||||
/// </summary>
|
||||
Up = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor down.
|
||||
/// </summary>
|
||||
Down = 3,
|
||||
}
|
||||
/// <summary>
|
||||
/// Moves the cursor down.
|
||||
/// </summary>
|
||||
Down = 3,
|
||||
}
|
||||
|
|
|
@ -1,102 +1,99 @@
|
|||
using System;
|
||||
using Mono.Unix.Native;
|
||||
using static Spectre.Terminals.Drivers.LinuxInterop;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed class LinuxDriver : UnixDriver
|
||||
{
|
||||
internal sealed class LinuxDriver : UnixDriver
|
||||
private termios? _original;
|
||||
private termios? _current;
|
||||
|
||||
public override string Name { get; } = "Linux";
|
||||
|
||||
public LinuxDriver()
|
||||
{
|
||||
private termios? _original;
|
||||
private termios? _current;
|
||||
|
||||
public override string Name { get; } = "Linux";
|
||||
|
||||
public LinuxDriver()
|
||||
if (tcgetattr(UnixConstants.STDIN, out var settings) == 0)
|
||||
{
|
||||
if (tcgetattr(UnixConstants.STDIN, out var settings) == 0)
|
||||
{
|
||||
// These values are usually the default, but we set them just to be safe.
|
||||
settings.c_cc[VTIME] = 0;
|
||||
settings.c_cc[VMIN] = 1;
|
||||
// These values are usually the default, but we set them just to be safe.
|
||||
settings.c_cc[VTIME] = 0;
|
||||
settings.c_cc[VMIN] = 1;
|
||||
|
||||
_original = settings;
|
||||
_original = settings;
|
||||
|
||||
// We might get really unlucky and fail to apply the settings right after the call
|
||||
// above. We should still assign _current so we can apply it later.
|
||||
if (!UpdateSettings(TCSANOW, settings))
|
||||
{
|
||||
_current = settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool EnableRawMode()
|
||||
{
|
||||
return SetRawMode(true);
|
||||
}
|
||||
|
||||
public override bool DisableRawMode()
|
||||
{
|
||||
return SetRawMode(false);
|
||||
}
|
||||
|
||||
public override void RefreshSettings()
|
||||
{
|
||||
if (_current is termios settings)
|
||||
{
|
||||
// This call can fail if the terminal is detached, but that is OK.
|
||||
UpdateSettings(TCSANOW, settings);
|
||||
}
|
||||
}
|
||||
|
||||
public override TerminalSize? GetTerminalSize()
|
||||
{
|
||||
var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w);
|
||||
if (result == 0)
|
||||
{
|
||||
return new TerminalSize(w.ws_col, w.ws_row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool SetRawMode(bool raw)
|
||||
{
|
||||
if (_original is not termios settings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (raw)
|
||||
{
|
||||
settings.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
||||
settings.c_oflag &= ~OPOST;
|
||||
settings.c_cflag &= ~(CSIZE | PARENB);
|
||||
settings.c_cflag |= CS8;
|
||||
settings.c_lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN);
|
||||
}
|
||||
|
||||
return UpdateSettings(TCSAFLUSH, settings) ? true :
|
||||
throw new InvalidOperationException(
|
||||
$"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}");
|
||||
}
|
||||
|
||||
private bool UpdateSettings(int mode, termios settings)
|
||||
{
|
||||
int result;
|
||||
while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1
|
||||
&& Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
// We might get really unlucky and fail to apply the settings right after the call
|
||||
// above. We should still assign _current so we can apply it later.
|
||||
if (!UpdateSettings(TCSANOW, settings))
|
||||
{
|
||||
_current = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool EnableRawMode()
|
||||
{
|
||||
return SetRawMode(true);
|
||||
}
|
||||
|
||||
public override bool DisableRawMode()
|
||||
{
|
||||
return SetRawMode(false);
|
||||
}
|
||||
|
||||
public override void RefreshSettings()
|
||||
{
|
||||
if (_current is termios settings)
|
||||
{
|
||||
// This call can fail if the terminal is detached, but that is OK.
|
||||
UpdateSettings(TCSANOW, settings);
|
||||
}
|
||||
}
|
||||
|
||||
public override TerminalSize? GetTerminalSize()
|
||||
{
|
||||
var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w);
|
||||
if (result == 0)
|
||||
{
|
||||
return new TerminalSize(w.ws_col, w.ws_row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool SetRawMode(bool raw)
|
||||
{
|
||||
if (_original is not termios settings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (raw)
|
||||
{
|
||||
settings.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
||||
settings.c_oflag &= ~OPOST;
|
||||
settings.c_cflag &= ~(CSIZE | PARENB);
|
||||
settings.c_cflag |= CS8;
|
||||
settings.c_lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN);
|
||||
}
|
||||
|
||||
return UpdateSettings(TCSAFLUSH, settings) ? true :
|
||||
throw new InvalidOperationException(
|
||||
$"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}");
|
||||
}
|
||||
|
||||
private bool UpdateSettings(int mode, termios settings)
|
||||
{
|
||||
int result;
|
||||
while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1
|
||||
&& Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
_current = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +1,152 @@
|
|||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed partial class LinuxInterop
|
||||
{
|
||||
internal sealed partial class LinuxInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// winsize: Fill in the winsize structure pointed to by the
|
||||
/// third argument with the screen width and height.
|
||||
/// </summary>
|
||||
public const uint TIOCGWINSZ = 0x5413;
|
||||
/// <summary>
|
||||
/// winsize: Fill in the winsize structure pointed to by the
|
||||
/// third argument with the screen width and height.
|
||||
/// </summary>
|
||||
public const uint TIOCGWINSZ = 0x5413;
|
||||
|
||||
/// <summary>
|
||||
/// termio: Size of the control character array.
|
||||
/// </summary>
|
||||
public const int NCCS = 32;
|
||||
/// <summary>
|
||||
/// termio: Size of the control character array.
|
||||
/// </summary>
|
||||
public const int NCCS = 32;
|
||||
|
||||
/// <summary>
|
||||
/// termio: Apply changes immediately.
|
||||
/// </summary>
|
||||
public const int TCSANOW = 0;
|
||||
/// <summary>
|
||||
/// termio: Apply changes immediately.
|
||||
/// </summary>
|
||||
public const int TCSANOW = 0;
|
||||
|
||||
/// <summary>
|
||||
/// termio: the change occurs after all output written to the object
|
||||
/// referred by fd has been transmitted, and all input that
|
||||
/// has been received but not read will be discarded before
|
||||
/// the change is made.
|
||||
/// </summary>
|
||||
public const int TCSAFLUSH = 2;
|
||||
/// <summary>
|
||||
/// termio: the change occurs after all output written to the object
|
||||
/// referred by fd has been transmitted, and all input that
|
||||
/// has been received but not read will be discarded before
|
||||
/// the change is made.
|
||||
/// </summary>
|
||||
public const int TCSAFLUSH = 2;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore BREAK condition on input.
|
||||
/// </summary>
|
||||
public const uint IGNBRK = 0x1;
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore BREAK condition on input.
|
||||
/// </summary>
|
||||
public const uint IGNBRK = 0x1;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set
|
||||
/// but BRKINT is set, then a BREAK causes the input and
|
||||
/// output queues to be flushed, and if the terminal is the
|
||||
/// controlling terminal of a foreground process group, it
|
||||
/// will cause a SIGINT to be sent to this foreground process
|
||||
/// group.When neither IGNBRK nor BRKINT are set, a BREAK
|
||||
/// reads as a null byte <c>('\0')</c>, except when PARMRK is set, in
|
||||
/// which case it reads as the sequence <c>\377 \0 \0</c>.
|
||||
/// </summary>
|
||||
public const uint BRKINT = 0x2;
|
||||
/// <summary>
|
||||
/// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set
|
||||
/// but BRKINT is set, then a BREAK causes the input and
|
||||
/// output queues to be flushed, and if the terminal is the
|
||||
/// controlling terminal of a foreground process group, it
|
||||
/// will cause a SIGINT to be sent to this foreground process
|
||||
/// group.When neither IGNBRK nor BRKINT are set, a BREAK
|
||||
/// reads as a null byte <c>('\0')</c>, except when PARMRK is set, in
|
||||
/// which case it reads as the sequence <c>\377 \0 \0</c>.
|
||||
/// </summary>
|
||||
public const uint BRKINT = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: If this bit is set, input bytes with parity or framing
|
||||
/// errors are marked when passed to the program. This bit is
|
||||
/// meaningful only when INPCK is set and IGNPAR is not set.
|
||||
/// The way erroneous bytes are marked is with two preceding
|
||||
/// bytes, \377 and \0. Thus, the program actually reads
|
||||
/// three bytes for one erroneous byte received from the
|
||||
/// terminal. If a valid byte has the value \377, and ISTRIP
|
||||
/// (see below) is not set, the program might confuse it with
|
||||
/// the prefix that marks a parity error. Therefore, a valid
|
||||
/// byte \377 is passed to the program as two bytes, \377
|
||||
/// \377, in this case.
|
||||
///
|
||||
/// If neither IGNPAR nor PARMRK is set, read a character with
|
||||
/// a parity error or framing error as \0.
|
||||
/// </summary>
|
||||
public const uint PARMRK = 0x8;
|
||||
/// <summary>
|
||||
/// c_iflag: If this bit is set, input bytes with parity or framing
|
||||
/// errors are marked when passed to the program. This bit is
|
||||
/// meaningful only when INPCK is set and IGNPAR is not set.
|
||||
/// The way erroneous bytes are marked is with two preceding
|
||||
/// bytes, \377 and \0. Thus, the program actually reads
|
||||
/// three bytes for one erroneous byte received from the
|
||||
/// terminal. If a valid byte has the value \377, and ISTRIP
|
||||
/// (see below) is not set, the program might confuse it with
|
||||
/// the prefix that marks a parity error. Therefore, a valid
|
||||
/// byte \377 is passed to the program as two bytes, \377
|
||||
/// \377, in this case.
|
||||
///
|
||||
/// If neither IGNPAR nor PARMRK is set, read a character with
|
||||
/// a parity error or framing error as \0.
|
||||
/// </summary>
|
||||
public const uint PARMRK = 0x8;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Strip off eighth bit.
|
||||
/// </summary>
|
||||
public const uint ISTRIP = 0x20;
|
||||
/// <summary>
|
||||
/// c_iflag: Strip off eighth bit.
|
||||
/// </summary>
|
||||
public const uint ISTRIP = 0x20;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Translate NL to CR on input.
|
||||
/// </summary>
|
||||
public const uint INLCR = 0x40;
|
||||
/// <summary>
|
||||
/// c_iflag: Translate NL to CR on input.
|
||||
/// </summary>
|
||||
public const uint INLCR = 0x40;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore carriage return on input.
|
||||
/// </summary>
|
||||
public const uint IGNCR = 0x80;
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore carriage return on input.
|
||||
/// </summary>
|
||||
public const uint IGNCR = 0x80;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Translate carriage return to newline on input (unless IGNCR is set).
|
||||
/// </summary>
|
||||
public const uint ICRNL = 0x100;
|
||||
/// <summary>
|
||||
/// c_iflag: Translate carriage return to newline on input (unless IGNCR is set).
|
||||
/// </summary>
|
||||
public const uint ICRNL = 0x100;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input.
|
||||
/// </summary>
|
||||
public const uint IXON = 0x400;
|
||||
/// <summary>
|
||||
/// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input.
|
||||
/// </summary>
|
||||
public const uint IXON = 0x400;
|
||||
|
||||
/// <summary>
|
||||
/// c_oflag: Enable implementation-defined output processing.
|
||||
/// </summary>
|
||||
public const uint OPOST = 0x1;
|
||||
/// <summary>
|
||||
/// c_oflag: Enable implementation-defined output processing.
|
||||
/// </summary>
|
||||
public const uint OPOST = 0x1;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8.
|
||||
/// </summary>
|
||||
public const uint CSIZE = 0x30;
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8.
|
||||
/// </summary>
|
||||
public const uint CSIZE = 0x30;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask: 8 bit.
|
||||
/// </summary>
|
||||
public const uint CS8 = 0x30;
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask: 8 bit.
|
||||
/// </summary>
|
||||
public const uint CS8 = 0x30;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Enable parity generation on output and parity checking for input.
|
||||
/// </summary>
|
||||
public const uint PARENB = 0x100;
|
||||
/// <summary>
|
||||
/// c_cflag: Enable parity generation on output and parity checking for input.
|
||||
/// </summary>
|
||||
public const uint PARENB = 0x100;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are
|
||||
/// received, generate the corresponding signal.
|
||||
/// </summary>
|
||||
public const uint ISIG = 0x1;
|
||||
/// <summary>
|
||||
/// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are
|
||||
/// received, generate the corresponding signal.
|
||||
/// </summary>
|
||||
public const uint ISIG = 0x1;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Enable canonical mode.
|
||||
/// Input is made available line by line.
|
||||
/// An input line is available when one of the line delimiters is typed
|
||||
/// (NL, EOL, EOL2; or EOF at the start of line).
|
||||
/// Line editing is enabled.
|
||||
/// The maximum line length is 4096 chars (including the terminating newline character).
|
||||
/// </summary>
|
||||
public const uint ICANON = 0x2;
|
||||
/// <summary>
|
||||
/// c_lflag: Enable canonical mode.
|
||||
/// Input is made available line by line.
|
||||
/// An input line is available when one of the line delimiters is typed
|
||||
/// (NL, EOL, EOL2; or EOF at the start of line).
|
||||
/// Line editing is enabled.
|
||||
/// The maximum line length is 4096 chars (including the terminating newline character).
|
||||
/// </summary>
|
||||
public const uint ICANON = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Echo input characters.
|
||||
/// </summary>
|
||||
public const uint ECHO = 0x8;
|
||||
/// <summary>
|
||||
/// c_lflag: Echo input characters.
|
||||
/// </summary>
|
||||
public const uint ECHO = 0x8;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set.
|
||||
/// </summary>
|
||||
public const uint ECHONL = 0x40;
|
||||
/// <summary>
|
||||
/// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set.
|
||||
/// </summary>
|
||||
public const uint ECHONL = 0x40;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Enable implementation-defined input processing.
|
||||
/// This flag, as well as ICANON must be enabled for the special
|
||||
/// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted,
|
||||
/// and for the IUCLC flag to be effective.
|
||||
/// </summary>
|
||||
public const uint IEXTEN = 0x8000;
|
||||
/// <summary>
|
||||
/// c_lflag: Enable implementation-defined input processing.
|
||||
/// This flag, as well as ICANON must be enabled for the special
|
||||
/// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted,
|
||||
/// and for the IUCLC flag to be effective.
|
||||
/// </summary>
|
||||
public const uint IEXTEN = 0x8000;
|
||||
|
||||
/// <summary>
|
||||
/// c_cc: Timeout in deciseconds for noncanonical read (TIME).
|
||||
/// </summary>
|
||||
public const int VTIME = 5;
|
||||
/// <summary>
|
||||
/// c_cc: Timeout in deciseconds for noncanonical read (TIME).
|
||||
/// </summary>
|
||||
public const int VTIME = 5;
|
||||
|
||||
/// <summary>
|
||||
/// c_cc: Minimum number of characters for noncanonical read (MIN).
|
||||
/// </summary>
|
||||
public const int VMIN = 6;
|
||||
}
|
||||
/// <summary>
|
||||
/// c_cc: Minimum number of characters for noncanonical read (MIN).
|
||||
/// </summary>
|
||||
public const int VMIN = 6;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
{
|
||||
internal sealed partial class LinuxInterop
|
||||
|
|
|
@ -1,112 +1,109 @@
|
|||
using System;
|
||||
using Mono.Unix.Native;
|
||||
using static Spectre.Terminals.Drivers.MacOSInterop;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed class MacOSDriver : UnixDriver
|
||||
{
|
||||
internal sealed class MacOSDriver : UnixDriver
|
||||
private termios? _original;
|
||||
private termios? _current;
|
||||
|
||||
public override string Name => "macOS";
|
||||
|
||||
public MacOSDriver()
|
||||
{
|
||||
private termios? _original;
|
||||
private termios? _current;
|
||||
|
||||
public override string Name => "macOS";
|
||||
|
||||
public MacOSDriver()
|
||||
if (tcgetattr(UnixConstants.STDIN, out var settings) == 0)
|
||||
{
|
||||
if (tcgetattr(UnixConstants.STDIN, out var settings) == 0)
|
||||
{
|
||||
// These values are usually the default, but we set them just to be safe.
|
||||
settings.c_cc[VTIME] = 0;
|
||||
settings.c_cc[VMIN] = 1;
|
||||
// These values are usually the default, but we set them just to be safe.
|
||||
settings.c_cc[VTIME] = 0;
|
||||
settings.c_cc[VMIN] = 1;
|
||||
|
||||
_original = settings;
|
||||
_original = settings;
|
||||
|
||||
// We might get really unlucky and fail to apply the settings right after the call
|
||||
// above. We should still assign _current so we can apply it later.
|
||||
if (!UpdateSettings(TCSANOW, settings))
|
||||
{
|
||||
_current = settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool EnableRawMode()
|
||||
{
|
||||
return SetRawMode(true);
|
||||
}
|
||||
|
||||
public override bool DisableRawMode()
|
||||
{
|
||||
return SetRawMode(false);
|
||||
}
|
||||
|
||||
private bool SetRawMode(bool raw)
|
||||
{
|
||||
if (_original is not termios settings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (raw)
|
||||
{
|
||||
var iflag = (uint)settings.c_iflag;
|
||||
var oflag = (uint)settings.c_oflag;
|
||||
var cflag = (uint)settings.c_cflag;
|
||||
var lflag = (uint)settings.c_lflag;
|
||||
|
||||
iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
||||
oflag &= ~OPOST;
|
||||
cflag &= ~(CSIZE | PARENB);
|
||||
cflag |= CS8;
|
||||
lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN);
|
||||
|
||||
settings.c_iflag = (UIntPtr)iflag;
|
||||
settings.c_oflag = (UIntPtr)oflag;
|
||||
settings.c_cflag = (UIntPtr)cflag;
|
||||
settings.c_lflag = (UIntPtr)lflag;
|
||||
}
|
||||
|
||||
return UpdateSettings(TCSAFLUSH, settings) ? true :
|
||||
throw new InvalidOperationException(
|
||||
$"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}");
|
||||
}
|
||||
|
||||
public override TerminalSize? GetTerminalSize()
|
||||
{
|
||||
var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w);
|
||||
if (result == 0)
|
||||
{
|
||||
return new TerminalSize(w.ws_col, w.ws_row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void RefreshSettings()
|
||||
{
|
||||
if (_current is termios settings)
|
||||
{
|
||||
// This call can fail if the terminal is detached, but that is OK.
|
||||
UpdateSettings(TCSANOW, settings);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateSettings(int mode, termios settings)
|
||||
{
|
||||
int result;
|
||||
while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1
|
||||
&& Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
// We might get really unlucky and fail to apply the settings right after the call
|
||||
// above. We should still assign _current so we can apply it later.
|
||||
if (!UpdateSettings(TCSANOW, settings))
|
||||
{
|
||||
_current = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool EnableRawMode()
|
||||
{
|
||||
return SetRawMode(true);
|
||||
}
|
||||
|
||||
public override bool DisableRawMode()
|
||||
{
|
||||
return SetRawMode(false);
|
||||
}
|
||||
|
||||
private bool SetRawMode(bool raw)
|
||||
{
|
||||
if (_original is not termios settings)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (raw)
|
||||
{
|
||||
var iflag = (uint)settings.c_iflag;
|
||||
var oflag = (uint)settings.c_oflag;
|
||||
var cflag = (uint)settings.c_cflag;
|
||||
var lflag = (uint)settings.c_lflag;
|
||||
|
||||
iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
||||
oflag &= ~OPOST;
|
||||
cflag &= ~(CSIZE | PARENB);
|
||||
cflag |= CS8;
|
||||
lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN);
|
||||
|
||||
settings.c_iflag = (UIntPtr)iflag;
|
||||
settings.c_oflag = (UIntPtr)oflag;
|
||||
settings.c_cflag = (UIntPtr)cflag;
|
||||
settings.c_lflag = (UIntPtr)lflag;
|
||||
}
|
||||
|
||||
return UpdateSettings(TCSAFLUSH, settings) ? true :
|
||||
throw new InvalidOperationException(
|
||||
$"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}");
|
||||
}
|
||||
|
||||
public override TerminalSize? GetTerminalSize()
|
||||
{
|
||||
var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w);
|
||||
if (result == 0)
|
||||
{
|
||||
return new TerminalSize(w.ws_col, w.ws_row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void RefreshSettings()
|
||||
{
|
||||
if (_current is termios settings)
|
||||
{
|
||||
// This call can fail if the terminal is detached, but that is OK.
|
||||
UpdateSettings(TCSANOW, settings);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateSettings(int mode, termios settings)
|
||||
{
|
||||
int result;
|
||||
while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1
|
||||
&& Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
_current = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +1,152 @@
|
|||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed partial class MacOSInterop
|
||||
{
|
||||
internal sealed partial class MacOSInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// winsize: Fill in the winsize structure pointed to by the
|
||||
/// third argument with the screen width and height.
|
||||
/// </summary>
|
||||
public const uint TIOCGWINSZ = 0x40087468;
|
||||
/// <summary>
|
||||
/// winsize: Fill in the winsize structure pointed to by the
|
||||
/// third argument with the screen width and height.
|
||||
/// </summary>
|
||||
public const uint TIOCGWINSZ = 0x40087468;
|
||||
|
||||
/// <summary>
|
||||
/// termio: Size of the control character array.
|
||||
/// </summary>
|
||||
public const int NCCS = 20;
|
||||
/// <summary>
|
||||
/// termio: Size of the control character array.
|
||||
/// </summary>
|
||||
public const int NCCS = 20;
|
||||
|
||||
/// <summary>
|
||||
/// termio: Apply changes immediately.
|
||||
/// </summary>
|
||||
public const int TCSANOW = 0;
|
||||
/// <summary>
|
||||
/// termio: Apply changes immediately.
|
||||
/// </summary>
|
||||
public const int TCSANOW = 0;
|
||||
|
||||
/// <summary>
|
||||
/// termio: the change occurs after all output written to the object
|
||||
/// referred by fd has been transmitted, and all input that
|
||||
/// has been received but not read will be discarded before
|
||||
/// the change is made.
|
||||
/// </summary>
|
||||
public const int TCSAFLUSH = 2;
|
||||
/// <summary>
|
||||
/// termio: the change occurs after all output written to the object
|
||||
/// referred by fd has been transmitted, and all input that
|
||||
/// has been received but not read will be discarded before
|
||||
/// the change is made.
|
||||
/// </summary>
|
||||
public const int TCSAFLUSH = 2;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore BREAK condition on input.
|
||||
/// </summary>
|
||||
public const uint IGNBRK = 0x1;
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore BREAK condition on input.
|
||||
/// </summary>
|
||||
public const uint IGNBRK = 0x1;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set
|
||||
/// but BRKINT is set, then a BREAK causes the input and
|
||||
/// output queues to be flushed, and if the terminal is the
|
||||
/// controlling terminal of a foreground process group, it
|
||||
/// will cause a SIGINT to be sent to this foreground process
|
||||
/// group.When neither IGNBRK nor BRKINT are set, a BREAK
|
||||
/// reads as a null byte <c>('\0')</c>, except when PARMRK is set, in
|
||||
/// which case it reads as the sequence <c>\377 \0 \0</c>.
|
||||
/// </summary>
|
||||
public const uint BRKINT = 0x2;
|
||||
/// <summary>
|
||||
/// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set
|
||||
/// but BRKINT is set, then a BREAK causes the input and
|
||||
/// output queues to be flushed, and if the terminal is the
|
||||
/// controlling terminal of a foreground process group, it
|
||||
/// will cause a SIGINT to be sent to this foreground process
|
||||
/// group.When neither IGNBRK nor BRKINT are set, a BREAK
|
||||
/// reads as a null byte <c>('\0')</c>, except when PARMRK is set, in
|
||||
/// which case it reads as the sequence <c>\377 \0 \0</c>.
|
||||
/// </summary>
|
||||
public const uint BRKINT = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: If this bit is set, input bytes with parity or framing
|
||||
/// errors are marked when passed to the program. This bit is
|
||||
/// meaningful only when INPCK is set and IGNPAR is not set.
|
||||
/// The way erroneous bytes are marked is with two preceding
|
||||
/// bytes, \377 and \0. Thus, the program actually reads
|
||||
/// three bytes for one erroneous byte received from the
|
||||
/// terminal. If a valid byte has the value \377, and ISTRIP
|
||||
/// (see below) is not set, the program might confuse it with
|
||||
/// the prefix that marks a parity error. Therefore, a valid
|
||||
/// byte \377 is passed to the program as two bytes, \377
|
||||
/// \377, in this case.
|
||||
///
|
||||
/// If neither IGNPAR nor PARMRK is set, read a character with
|
||||
/// a parity error or framing error as \0.
|
||||
/// </summary>
|
||||
public const uint PARMRK = 0x8;
|
||||
/// <summary>
|
||||
/// c_iflag: If this bit is set, input bytes with parity or framing
|
||||
/// errors are marked when passed to the program. This bit is
|
||||
/// meaningful only when INPCK is set and IGNPAR is not set.
|
||||
/// The way erroneous bytes are marked is with two preceding
|
||||
/// bytes, \377 and \0. Thus, the program actually reads
|
||||
/// three bytes for one erroneous byte received from the
|
||||
/// terminal. If a valid byte has the value \377, and ISTRIP
|
||||
/// (see below) is not set, the program might confuse it with
|
||||
/// the prefix that marks a parity error. Therefore, a valid
|
||||
/// byte \377 is passed to the program as two bytes, \377
|
||||
/// \377, in this case.
|
||||
///
|
||||
/// If neither IGNPAR nor PARMRK is set, read a character with
|
||||
/// a parity error or framing error as \0.
|
||||
/// </summary>
|
||||
public const uint PARMRK = 0x8;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Strip off eighth bit.
|
||||
/// </summary>
|
||||
public const uint ISTRIP = 0x20;
|
||||
/// <summary>
|
||||
/// c_iflag: Strip off eighth bit.
|
||||
/// </summary>
|
||||
public const uint ISTRIP = 0x20;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Translate NL to CR on input.
|
||||
/// </summary>
|
||||
public const uint INLCR = 0x40;
|
||||
/// <summary>
|
||||
/// c_iflag: Translate NL to CR on input.
|
||||
/// </summary>
|
||||
public const uint INLCR = 0x40;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore carriage return on input.
|
||||
/// </summary>
|
||||
public const uint IGNCR = 0x80;
|
||||
/// <summary>
|
||||
/// c_iflag: Ignore carriage return on input.
|
||||
/// </summary>
|
||||
public const uint IGNCR = 0x80;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: Translate carriage return to newline on input (unless IGNCR is set).
|
||||
/// </summary>
|
||||
public const uint ICRNL = 0x100;
|
||||
/// <summary>
|
||||
/// c_iflag: Translate carriage return to newline on input (unless IGNCR is set).
|
||||
/// </summary>
|
||||
public const uint ICRNL = 0x100;
|
||||
|
||||
/// <summary>
|
||||
/// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input.
|
||||
/// </summary>
|
||||
public const uint IXON = 0x200;
|
||||
/// <summary>
|
||||
/// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input.
|
||||
/// </summary>
|
||||
public const uint IXON = 0x200;
|
||||
|
||||
/// <summary>
|
||||
/// c_oflag: Enable implementation-defined output processing.
|
||||
/// </summary>
|
||||
public const uint OPOST = 0x1;
|
||||
/// <summary>
|
||||
/// c_oflag: Enable implementation-defined output processing.
|
||||
/// </summary>
|
||||
public const uint OPOST = 0x1;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8.
|
||||
/// </summary>
|
||||
public const uint CSIZE = 0x300;
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8.
|
||||
/// </summary>
|
||||
public const uint CSIZE = 0x300;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask: 8 bit.
|
||||
/// </summary>
|
||||
public const uint CS8 = 0x300;
|
||||
/// <summary>
|
||||
/// c_cflag: Character size mask: 8 bit.
|
||||
/// </summary>
|
||||
public const uint CS8 = 0x300;
|
||||
|
||||
/// <summary>
|
||||
/// c_cflag: Enable parity generation on output and parity checking for input.
|
||||
/// </summary>
|
||||
public const uint PARENB = 0x1000;
|
||||
/// <summary>
|
||||
/// c_cflag: Enable parity generation on output and parity checking for input.
|
||||
/// </summary>
|
||||
public const uint PARENB = 0x1000;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are
|
||||
/// received, generate the corresponding signal.
|
||||
/// </summary>
|
||||
public const uint ISIG = 0x80;
|
||||
/// <summary>
|
||||
/// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are
|
||||
/// received, generate the corresponding signal.
|
||||
/// </summary>
|
||||
public const uint ISIG = 0x80;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Enable canonical mode.
|
||||
/// Input is made available line by line.
|
||||
/// An input line is available when one of the line delimiters is typed
|
||||
/// (NL, EOL, EOL2; or EOF at the start of line).
|
||||
/// Line editing is enabled.
|
||||
/// The maximum line length is 4096 chars (including the terminating newline character).
|
||||
/// </summary>
|
||||
public const uint ICANON = 0x100;
|
||||
/// <summary>
|
||||
/// c_lflag: Enable canonical mode.
|
||||
/// Input is made available line by line.
|
||||
/// An input line is available when one of the line delimiters is typed
|
||||
/// (NL, EOL, EOL2; or EOF at the start of line).
|
||||
/// Line editing is enabled.
|
||||
/// The maximum line length is 4096 chars (including the terminating newline character).
|
||||
/// </summary>
|
||||
public const uint ICANON = 0x100;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Echo input characters.
|
||||
/// </summary>
|
||||
public const uint ECHO = 0x8;
|
||||
/// <summary>
|
||||
/// c_lflag: Echo input characters.
|
||||
/// </summary>
|
||||
public const uint ECHO = 0x8;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set.
|
||||
/// </summary>
|
||||
public const uint ECHONL = 0x10;
|
||||
/// <summary>
|
||||
/// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set.
|
||||
/// </summary>
|
||||
public const uint ECHONL = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// c_lflag: Enable implementation-defined input processing.
|
||||
/// This flag, as well as ICANON must be enabled for the special
|
||||
/// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted,
|
||||
/// and for the IUCLC flag to be effective.
|
||||
/// </summary>
|
||||
public const uint IEXTEN = 0x400;
|
||||
/// <summary>
|
||||
/// c_lflag: Enable implementation-defined input processing.
|
||||
/// This flag, as well as ICANON must be enabled for the special
|
||||
/// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted,
|
||||
/// and for the IUCLC flag to be effective.
|
||||
/// </summary>
|
||||
public const uint IEXTEN = 0x400;
|
||||
|
||||
/// <summary>
|
||||
/// c_cc: Timeout in deciseconds for noncanonical read (TIME).
|
||||
/// </summary>
|
||||
public const int VTIME = 17;
|
||||
/// <summary>
|
||||
/// c_cc: Timeout in deciseconds for noncanonical read (TIME).
|
||||
/// </summary>
|
||||
public const int VTIME = 17;
|
||||
|
||||
/// <summary>
|
||||
/// c_cc: Minimum number of characters for noncanonical read (MIN).
|
||||
/// </summary>
|
||||
public const int VMIN = 16;
|
||||
}
|
||||
/// <summary>
|
||||
/// c_cc: Minimum number of characters for noncanonical read (MIN).
|
||||
/// </summary>
|
||||
public const int VMIN = 16;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
{
|
||||
internal sealed partial class MacOSInterop
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed class UnixConstants
|
||||
{
|
||||
internal sealed class UnixConstants
|
||||
{
|
||||
public const int STDIN = 0;
|
||||
public const int STDOUT = 1;
|
||||
public const int STDERR = 2;
|
||||
}
|
||||
public const int STDIN = 0;
|
||||
public const int STDOUT = 1;
|
||||
public const int STDERR = 2;
|
||||
}
|
||||
|
|
|
@ -1,145 +1,140 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Mono.Unix;
|
||||
using Mono.Unix.Native;
|
||||
using Syscall = Mono.Unix.Native.Syscall;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal abstract class UnixDriver : ITerminalDriver
|
||||
{
|
||||
internal abstract class UnixDriver : ITerminalDriver
|
||||
private readonly Thread _signalListenerThread;
|
||||
private readonly ManualResetEvent _stopEvent;
|
||||
private readonly ManualResetEvent _stoppedEvent;
|
||||
|
||||
public abstract string Name { get; }
|
||||
public bool IsRawMode { get; }
|
||||
public TerminalSize? Size { get; private set; }
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
|
||||
public ITerminalReader Input { get; }
|
||||
public ITerminalWriter Output { get; }
|
||||
public ITerminalWriter Error { get; }
|
||||
|
||||
protected UnixDriver()
|
||||
{
|
||||
private readonly Thread _signalListenerThread;
|
||||
private readonly ManualResetEvent _stopEvent;
|
||||
private readonly ManualResetEvent _stoppedEvent;
|
||||
Input = new UnixTerminalReader();
|
||||
Output = new UnixTerminalWriter(UnixConstants.STDIN);
|
||||
Error = new UnixTerminalWriter(UnixConstants.STDERR);
|
||||
|
||||
public abstract string Name { get; }
|
||||
public bool IsRawMode { get; }
|
||||
public TerminalSize? Size { get; private set; }
|
||||
Size = GetTerminalSize();
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
_stopEvent = new ManualResetEvent(false);
|
||||
_stoppedEvent = new ManualResetEvent(false);
|
||||
_signalListenerThread = CreateSignalListener(_stopEvent);
|
||||
_signalListenerThread.Start();
|
||||
}
|
||||
|
||||
public ITerminalReader Input { get; }
|
||||
public ITerminalWriter Output { get; }
|
||||
public ITerminalWriter Error { get; }
|
||||
|
||||
protected UnixDriver()
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_stoppedEvent.WaitOne(0))
|
||||
{
|
||||
Input = new UnixTerminalReader();
|
||||
Output = new UnixTerminalWriter(UnixConstants.STDIN);
|
||||
Error = new UnixTerminalWriter(UnixConstants.STDERR);
|
||||
_stopEvent.Set();
|
||||
_signalListenerThread.Join();
|
||||
_stoppedEvent.WaitOne();
|
||||
}
|
||||
}
|
||||
|
||||
Size = GetTerminalSize();
|
||||
public abstract bool EnableRawMode();
|
||||
public abstract bool DisableRawMode();
|
||||
public abstract void RefreshSettings();
|
||||
public abstract TerminalSize? GetTerminalSize();
|
||||
|
||||
_stopEvent = new ManualResetEvent(false);
|
||||
_stoppedEvent = new ManualResetEvent(false);
|
||||
_signalListenerThread = CreateSignalListener(_stopEvent);
|
||||
_signalListenerThread.Start();
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
switch (signal)
|
||||
{
|
||||
case TerminalSignal.SIGINT:
|
||||
_ = Syscall.kill(0, Signum.SIGINT);
|
||||
return true;
|
||||
case TerminalSignal.SIGQUIT:
|
||||
_ = Syscall.kill(0, Signum.SIGQUIT);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
return false;
|
||||
}
|
||||
|
||||
private Thread CreateSignalListener(WaitHandle stop)
|
||||
{
|
||||
return new Thread(() =>
|
||||
{
|
||||
if (!_stoppedEvent.WaitOne(0))
|
||||
using var sigWinch = new UnixSignal(Signum.SIGWINCH);
|
||||
using var sigCont = new UnixSignal(Signum.SIGCONT);
|
||||
using var sigInt = new UnixSignal(Signum.SIGINT);
|
||||
using var sigQuit = new UnixSignal(Signum.SIGQUIT);
|
||||
|
||||
var signals = new[] { sigWinch, sigCont, sigInt, sigQuit };
|
||||
while (!stop.WaitOne(0))
|
||||
{
|
||||
_stopEvent.Set();
|
||||
_signalListenerThread.Join();
|
||||
_stoppedEvent.WaitOne();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract bool EnableRawMode();
|
||||
public abstract bool DisableRawMode();
|
||||
public abstract void RefreshSettings();
|
||||
public abstract TerminalSize? GetTerminalSize();
|
||||
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
switch (signal)
|
||||
{
|
||||
case TerminalSignal.SIGINT:
|
||||
_ = Syscall.kill(0, Signum.SIGINT);
|
||||
return true;
|
||||
case TerminalSignal.SIGQUIT:
|
||||
_ = Syscall.kill(0, Signum.SIGQUIT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Thread CreateSignalListener(WaitHandle stop)
|
||||
{
|
||||
return new Thread(() =>
|
||||
{
|
||||
using var sigWinch = new UnixSignal(Signum.SIGWINCH);
|
||||
using var sigCont = new UnixSignal(Signum.SIGCONT);
|
||||
using var sigInt = new UnixSignal(Signum.SIGINT);
|
||||
using var sigQuit = new UnixSignal(Signum.SIGQUIT);
|
||||
|
||||
var signals = new[] { sigWinch, sigCont, sigInt, sigQuit };
|
||||
while (!stop.WaitOne(0))
|
||||
var index = UnixSignal.WaitAny(signals, TimeSpan.FromMilliseconds(250));
|
||||
if (index == 250)
|
||||
{
|
||||
var index = UnixSignal.WaitAny(signals, TimeSpan.FromMilliseconds(250));
|
||||
if (index == 250)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (index == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var signal = signals[index];
|
||||
if (signal == stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var signal = signals[index];
|
||||
if (signal == stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are being restored from the background (SIGCONT), it is possible that
|
||||
// terminal settings have been mangled, so restore them.
|
||||
if (signal == sigCont)
|
||||
{
|
||||
RefreshSettings();
|
||||
}
|
||||
// If we are being restored from the background (SIGCONT), it is possible that
|
||||
// terminal settings have been mangled, so restore them.
|
||||
if (signal == sigCont)
|
||||
{
|
||||
RefreshSettings();
|
||||
}
|
||||
|
||||
// Terminal width/height might have changed for SIGCONT, and will definitely
|
||||
// have changed for SIGWINCH.
|
||||
if (signal == sigCont || signal == sigWinch)
|
||||
{
|
||||
Size = GetTerminalSize();
|
||||
}
|
||||
// Terminal width/height might have changed for SIGCONT, and will definitely
|
||||
// have changed for SIGWINCH.
|
||||
if (signal == sigCont || signal == sigWinch)
|
||||
{
|
||||
Size = GetTerminalSize();
|
||||
}
|
||||
|
||||
if (signal == sigQuit || signal == sigInt)
|
||||
if (signal == sigQuit || signal == sigInt)
|
||||
{
|
||||
if (Signalled != null)
|
||||
{
|
||||
if (Signalled != null)
|
||||
// Propaate the signal
|
||||
var received = signal == sigQuit ? TerminalSignal.SIGQUIT : TerminalSignal.SIGINT;
|
||||
var signalEventArguments = new TerminalSignalEventArgs(received);
|
||||
Signalled(null, new TerminalSignalEventArgs(received));
|
||||
|
||||
// Not cancelled?
|
||||
if (!signalEventArguments.Cancel)
|
||||
{
|
||||
// Propaate the signal
|
||||
var received = signal == sigQuit ? TerminalSignal.SIGQUIT : TerminalSignal.SIGINT;
|
||||
var signalEventArguments = new TerminalSignalEventArgs(received);
|
||||
Signalled(null, new TerminalSignalEventArgs(received));
|
||||
//// Get the value early to avoid ObjectDisposedException.
|
||||
var num = signal.Signum;
|
||||
|
||||
// Not cancelled?
|
||||
if (!signalEventArguments.Cancel)
|
||||
{
|
||||
//// Get the value early to avoid ObjectDisposedException.
|
||||
var num = signal.Signum;
|
||||
|
||||
//// Remove our signal handler and send the signal again. Since we
|
||||
//// have overwritten the signal handlers in CoreCLR and
|
||||
//// System.Native, this gives those handlers an opportunity to run.
|
||||
signal.Dispose();
|
||||
Syscall.kill(Syscall.getpid(), num);
|
||||
}
|
||||
//// Remove our signal handler and send the signal again. Since we
|
||||
//// have overwritten the signal handlers in CoreCLR and
|
||||
//// System.Native, this gives those handlers an opportunity to run.
|
||||
signal.Dispose();
|
||||
Syscall.kill(Syscall.getpid(), num);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stoppedEvent.Set();
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Signal Listener",
|
||||
};
|
||||
}
|
||||
_stoppedEvent.Set();
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Signal Listener",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,111 +1,105 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Mono.Unix.Native;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class UnixTerminalReader : ITerminalReader
|
||||
{
|
||||
internal sealed class UnixTerminalReader : ITerminalReader
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly Encoding _encoding;
|
||||
get => _encoding;
|
||||
set { /* Do nothing for now */ }
|
||||
}
|
||||
|
||||
public Encoding Encoding
|
||||
public bool IsKeyAvailable => throw new NotSupportedException("Not yet supported");
|
||||
|
||||
public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN);
|
||||
|
||||
public UnixTerminalReader()
|
||||
{
|
||||
_encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
public string? ReadLine()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
private static unsafe int Read(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
get => _encoding;
|
||||
set { /* Do nothing for now */ }
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool IsKeyAvailable => throw new NotSupportedException("Not yet supported");
|
||||
long ret;
|
||||
|
||||
public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN);
|
||||
|
||||
public UnixTerminalReader()
|
||||
while (true)
|
||||
{
|
||||
_encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
public string? ReadLine()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
throw new NotSupportedException("Not yet supported");
|
||||
}
|
||||
|
||||
private static unsafe int Read(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long ret;
|
||||
|
||||
while (true)
|
||||
{
|
||||
fixed (byte* p = buffer)
|
||||
while ((ret = Syscall.read(UnixConstants.STDIN, p, (ulong)buffer.Length)) == -1 &&
|
||||
Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
while ((ret = Syscall.read(UnixConstants.STDIN, p, (ulong)buffer.Length)) == -1 &&
|
||||
Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
|
||||
if (ret != -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var err = Stdlib.GetLastError();
|
||||
|
||||
// The descriptor was probably redirected to a program that ended. Just
|
||||
// silently ignore this situation.
|
||||
//
|
||||
// The strange condition where errno is zero happens e.g. on Linux if
|
||||
// the process is killed while blocking in the read system call.
|
||||
if (err == 0 || err == Errno.EPIPE)
|
||||
{
|
||||
ret = 0;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// The file descriptor has been configured as non-blocking. Instead of
|
||||
// busily trying to read over and over, poll until we can write and then
|
||||
// try again.
|
||||
if (err == Errno.EAGAIN)
|
||||
{
|
||||
_ = Syscall.poll(
|
||||
new[]
|
||||
{
|
||||
new Pollfd
|
||||
{
|
||||
fd = UnixConstants.STDIN,
|
||||
events = PollEvents.POLLIN,
|
||||
},
|
||||
}, 1, Timeout.Infinite);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err == 0)
|
||||
{
|
||||
err = Errno.EBADF;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not read from STDIN: {Stdlib.strerror(err)}");
|
||||
// Retry in case we get interrupted by a signal.
|
||||
}
|
||||
}
|
||||
|
||||
return (int)ret;
|
||||
if (ret != -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var err = Stdlib.GetLastError();
|
||||
|
||||
// The descriptor was probably redirected to a program that ended. Just
|
||||
// silently ignore this situation.
|
||||
//
|
||||
// The strange condition where errno is zero happens e.g. on Linux if
|
||||
// the process is killed while blocking in the read system call.
|
||||
if (err == 0 || err == Errno.EPIPE)
|
||||
{
|
||||
ret = 0;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// The file descriptor has been configured as non-blocking. Instead of
|
||||
// busily trying to read over and over, poll until we can write and then
|
||||
// try again.
|
||||
if (err == Errno.EAGAIN)
|
||||
{
|
||||
_ = Syscall.poll(
|
||||
new[]
|
||||
{
|
||||
new Pollfd
|
||||
{
|
||||
fd = UnixConstants.STDIN,
|
||||
events = PollEvents.POLLIN,
|
||||
},
|
||||
}, 1, Timeout.Infinite);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err == 0)
|
||||
{
|
||||
err = Errno.EBADF;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not read from STDIN: {Stdlib.strerror(err)}");
|
||||
}
|
||||
}
|
||||
|
||||
return (int)ret;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,93 +1,87 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Mono.Unix.Native;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class UnixTerminalWriter : ITerminalWriter
|
||||
{
|
||||
internal sealed class UnixTerminalWriter : ITerminalWriter
|
||||
private readonly int _handle;
|
||||
private readonly string _name;
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly int _handle;
|
||||
private readonly string _name;
|
||||
private readonly Encoding _encoding;
|
||||
get => _encoding;
|
||||
set { /* Do nothing for now */ }
|
||||
}
|
||||
|
||||
public Encoding Encoding
|
||||
public bool IsRedirected => !Syscall.isatty(_handle);
|
||||
|
||||
public UnixTerminalWriter(int handle)
|
||||
{
|
||||
_handle = handle;
|
||||
_name = handle == UnixConstants.STDIN ? "STDIN" : "STDERR";
|
||||
_encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
}
|
||||
|
||||
// From https://github.com/alexrp/system-terminal/blob/819090b722e3198b6b932fdd67641371be99e844/src/core/Drivers/UnixTerminalDriver.cs#L111
|
||||
public unsafe void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
get => _encoding;
|
||||
set { /* Do nothing for now */ }
|
||||
return;
|
||||
}
|
||||
|
||||
public bool IsRedirected => !Syscall.isatty(_handle);
|
||||
var progress = 0;
|
||||
|
||||
public UnixTerminalWriter(int handle)
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
_handle = handle;
|
||||
_name = handle == UnixConstants.STDIN ? "STDIN" : "STDERR";
|
||||
_encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
}
|
||||
var len = buffer.Length;
|
||||
|
||||
// From https://github.com/alexrp/system-terminal/blob/819090b722e3198b6b932fdd67641371be99e844/src/core/Drivers/UnixTerminalDriver.cs#L111
|
||||
public unsafe void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
while (progress < len)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = 0;
|
||||
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
var len = buffer.Length;
|
||||
|
||||
while (progress < len)
|
||||
long result;
|
||||
while ((result = Syscall.write(_handle, p + progress, (ulong)(len - progress))) == -1 &&
|
||||
Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
long result;
|
||||
while ((result = Syscall.write(_handle, p + progress, (ulong)(len - progress))) == -1 &&
|
||||
Stdlib.GetLastError() == Errno.EINTR)
|
||||
{
|
||||
}
|
||||
|
||||
// The descriptor has been closed by someone else. Just silently ignore
|
||||
// this situation.
|
||||
if (result == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != -1)
|
||||
{
|
||||
progress += (int)result;
|
||||
continue;
|
||||
}
|
||||
|
||||
var err = Stdlib.GetLastError();
|
||||
if (err == Errno.EPIPE)
|
||||
{
|
||||
// The descriptor was probably redirected to a program that ended. Just
|
||||
// silently ignore this situation.
|
||||
break;
|
||||
}
|
||||
else if (err == Errno.EAGAIN)
|
||||
{
|
||||
// The file descriptor has been configured as non-blocking. Instead of
|
||||
// busily trying to write over and over, poll until we can write and
|
||||
// then try again.
|
||||
Syscall.poll(
|
||||
new[]
|
||||
{
|
||||
new Pollfd
|
||||
{
|
||||
fd = _handle,
|
||||
events = PollEvents.POLLOUT,
|
||||
},
|
||||
}, 1, Timeout.Infinite);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Could not write to standard {_name}: {Stdlib.strerror(err)}");
|
||||
}
|
||||
|
||||
// The descriptor has been closed by someone else. Just silently ignore
|
||||
// this situation.
|
||||
if (result == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != -1)
|
||||
{
|
||||
progress += (int)result;
|
||||
continue;
|
||||
}
|
||||
|
||||
var err = Stdlib.GetLastError();
|
||||
if (err == Errno.EPIPE)
|
||||
{
|
||||
// The descriptor was probably redirected to a program that ended. Just
|
||||
// silently ignore this situation.
|
||||
break;
|
||||
}
|
||||
else if (err == Errno.EAGAIN)
|
||||
{
|
||||
// The file descriptor has been configured as non-blocking. Instead of
|
||||
// busily trying to write over and over, poll until we can write and
|
||||
// then try again.
|
||||
Syscall.poll(
|
||||
new[]
|
||||
{
|
||||
new Pollfd
|
||||
{
|
||||
fd = _handle,
|
||||
events = PollEvents.POLLOUT,
|
||||
},
|
||||
}, 1, Timeout.Infinite);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Could not write to standard {_name}: {Stdlib.strerror(err)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,185 +1,179 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
using Spectre.Terminals.Emulation;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsColors
|
||||
{
|
||||
internal sealed class WindowsColors
|
||||
private readonly SafeHandle _stdout;
|
||||
private readonly SafeHandle _stderr;
|
||||
private readonly Color[]? _colorTable;
|
||||
private readonly short? _defaultColors;
|
||||
|
||||
public WindowsColors(SafeHandle stdout, SafeHandle stderr)
|
||||
{
|
||||
private readonly SafeHandle _stdout;
|
||||
private readonly SafeHandle _stderr;
|
||||
private readonly Color[]? _colorTable;
|
||||
private readonly short? _defaultColors;
|
||||
_stdout = stdout ?? throw new ArgumentNullException(nameof(stdout));
|
||||
_stderr = stderr ?? throw new ArgumentNullException(nameof(stderr));
|
||||
|
||||
public WindowsColors(SafeHandle stdout, SafeHandle stderr)
|
||||
if (TryGetConsoleBuffer(out var buffer))
|
||||
{
|
||||
_stdout = stdout ?? throw new ArgumentNullException(nameof(stdout));
|
||||
_stderr = stderr ?? throw new ArgumentNullException(nameof(stderr));
|
||||
|
||||
if (TryGetConsoleBuffer(out var buffer))
|
||||
// Build the color table
|
||||
// We will use this to find the nearest color.
|
||||
_colorTable = new Color[16];
|
||||
for (var index = 0; index < 16; index++)
|
||||
{
|
||||
// Build the color table
|
||||
// We will use this to find the nearest color.
|
||||
_colorTable = new Color[16];
|
||||
for (var index = 0; index < 16; index++)
|
||||
{
|
||||
_colorTable[index] = new Color(
|
||||
(int)((GetValue(ref buffer.ColorTable, index) >> 16) & 0xff),
|
||||
(int)((GetValue(ref buffer.ColorTable, index) >> 8) & 0xff),
|
||||
(int)(GetValue(ref buffer.ColorTable, index) & 0xff));
|
||||
}
|
||||
|
||||
// Get the default colors
|
||||
_defaultColors = (byte)(buffer.wAttributes & WindowsConstants.Colors.COLOR_MASK);
|
||||
_colorTable[index] = new Color(
|
||||
(int)((GetValue(ref buffer.ColorTable, index) >> 16) & 0xff),
|
||||
(int)((GetValue(ref buffer.ColorTable, index) >> 8) & 0xff),
|
||||
(int)(GetValue(ref buffer.ColorTable, index) & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetForeground(Color color)
|
||||
{
|
||||
SetColor(color, foreground: true);
|
||||
// Get the default colors
|
||||
_defaultColors = (byte)(buffer.wAttributes & WindowsConstants.Colors.COLOR_MASK);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBackground(Color color)
|
||||
public void SetForeground(Color color)
|
||||
{
|
||||
SetColor(color, foreground: true);
|
||||
}
|
||||
|
||||
public void SetBackground(Color color)
|
||||
{
|
||||
SetColor(color, foreground: false);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (_defaultColors != null)
|
||||
{
|
||||
SetColor(color, foreground: false);
|
||||
PInvoke.SetConsoleTextAttribute(_stdout, (ushort)_defaultColors.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (_defaultColors != null)
|
||||
{
|
||||
PInvoke.SetConsoleTextAttribute(_stdout, (ushort)_defaultColors.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private uint GetValue(ref CONSOLE_SCREEN_BUFFER_INFOEX.__uint_16 value, int index)
|
||||
{
|
||||
private uint GetValue(ref CONSOLE_SCREEN_BUFFER_INFOEX.__uint_16 value, int index)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
return value[index];
|
||||
return value[index];
|
||||
#else
|
||||
return index switch
|
||||
{
|
||||
0 => value._0,
|
||||
1 => value._1,
|
||||
2 => value._2,
|
||||
3 => value._3,
|
||||
4 => value._4,
|
||||
5 => value._5,
|
||||
6 => value._6,
|
||||
7 => value._7,
|
||||
8 => value._8,
|
||||
9 => value._9,
|
||||
10 => value._10,
|
||||
11 => value._11,
|
||||
12 => value._12,
|
||||
13 => value._13,
|
||||
14 => value._14,
|
||||
15 => value._15,
|
||||
_ => throw new InvalidOperationException("Invalid __uint_16 index"),
|
||||
};
|
||||
return index switch
|
||||
{
|
||||
0 => value._0,
|
||||
1 => value._1,
|
||||
2 => value._2,
|
||||
3 => value._3,
|
||||
4 => value._4,
|
||||
5 => value._5,
|
||||
6 => value._6,
|
||||
7 => value._7,
|
||||
8 => value._8,
|
||||
9 => value._9,
|
||||
10 => value._10,
|
||||
11 => value._11,
|
||||
12 => value._12,
|
||||
13 => value._13,
|
||||
14 => value._14,
|
||||
15 => value._15,
|
||||
_ => throw new InvalidOperationException("Invalid __uint_16 index"),
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
private unsafe bool TryGetConsoleBuffer(out CONSOLE_SCREEN_BUFFER_INFOEX info)
|
||||
{
|
||||
info = default(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
info.cbSize = (uint)sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
|
||||
if (PInvoke.GetConsoleScreenBufferInfoEx(_stdout, ref info) ||
|
||||
PInvoke.GetConsoleScreenBufferInfoEx(_stderr, ref info))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryGetConsoleBuffer(out CONSOLE_SCREEN_BUFFER_INFOEX info)
|
||||
{
|
||||
info = default(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
info.cbSize = (uint)sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PInvoke.GetConsoleScreenBufferInfoEx(_stdout, ref info) ||
|
||||
PInvoke.GetConsoleScreenBufferInfoEx(_stderr, ref info))
|
||||
private void SetColor(Color color, bool foreground)
|
||||
{
|
||||
var number = color.Number;
|
||||
|
||||
if (color.IsRgb && number == null)
|
||||
{
|
||||
// TODO: Find the closest color
|
||||
return;
|
||||
}
|
||||
|
||||
if (number != null)
|
||||
{
|
||||
var colorNumber = number.Value;
|
||||
if (colorNumber >= 0 && colorNumber < 16)
|
||||
{
|
||||
return true;
|
||||
// Map the number to 4-bit color
|
||||
colorNumber = Map4BitColor(colorNumber);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetColor(Color color, bool foreground)
|
||||
{
|
||||
var number = color.Number;
|
||||
|
||||
if (color.IsRgb && number == null)
|
||||
else if (colorNumber < 256)
|
||||
{
|
||||
// TODO: Find the closest color
|
||||
// TODO: Support 256-bit colors
|
||||
return;
|
||||
}
|
||||
|
||||
if (number != null)
|
||||
var c = GetColorAttribute(colorNumber, !foreground);
|
||||
if (c == null)
|
||||
{
|
||||
var colorNumber = number.Value;
|
||||
if (colorNumber >= 0 && colorNumber < 16)
|
||||
{
|
||||
// Map the number to 4-bit color
|
||||
colorNumber = Map4BitColor(colorNumber);
|
||||
}
|
||||
else if (colorNumber < 256)
|
||||
{
|
||||
// TODO: Support 256-bit colors
|
||||
return;
|
||||
}
|
||||
|
||||
var c = GetColorAttribute(colorNumber, !foreground);
|
||||
if (c == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetConsoleBuffer(out var buffer))
|
||||
{
|
||||
var attrs = (short)buffer.wAttributes;
|
||||
if (foreground)
|
||||
{
|
||||
attrs &= ~WindowsConstants.Colors.FOREGROUND_MASK;
|
||||
}
|
||||
else
|
||||
{
|
||||
attrs &= ~WindowsConstants.Colors.BACKGROUND_MASK;
|
||||
}
|
||||
|
||||
attrs = (short)(((uint)(ushort)attrs) | (ushort)c.Value);
|
||||
PInvoke.SetConsoleTextAttribute(_stdout, (ushort)attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int? GetColorAttribute(int color, bool isBackground)
|
||||
{
|
||||
if ((color & ~0xf) != 0)
|
||||
{
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBackground)
|
||||
if (TryGetConsoleBuffer(out var buffer))
|
||||
{
|
||||
color <<= 4;
|
||||
var attrs = (short)buffer.wAttributes;
|
||||
if (foreground)
|
||||
{
|
||||
attrs &= ~WindowsConstants.Colors.FOREGROUND_MASK;
|
||||
}
|
||||
else
|
||||
{
|
||||
attrs &= ~WindowsConstants.Colors.BACKGROUND_MASK;
|
||||
}
|
||||
|
||||
attrs = (short)(((uint)(ushort)attrs) | (ushort)c.Value);
|
||||
PInvoke.SetConsoleTextAttribute(_stdout, (ushort)attrs);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private static int Map4BitColor(int number)
|
||||
{
|
||||
return number switch
|
||||
{
|
||||
0 => 0,
|
||||
1 => 4,
|
||||
2 => 2,
|
||||
3 => 6,
|
||||
4 => 1,
|
||||
5 => 5,
|
||||
6 => 3,
|
||||
7 => 7,
|
||||
8 => 8,
|
||||
9 => 12,
|
||||
10 => 10,
|
||||
11 => 14,
|
||||
12 => 9,
|
||||
13 => 13,
|
||||
14 => 11,
|
||||
15 => 15,
|
||||
_ => number,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static int? GetColorAttribute(int color, bool isBackground)
|
||||
{
|
||||
if ((color & ~0xf) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isBackground)
|
||||
{
|
||||
color <<= 4;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private static int Map4BitColor(int number)
|
||||
{
|
||||
return number switch
|
||||
{
|
||||
0 => 0,
|
||||
1 => 4,
|
||||
2 => 2,
|
||||
3 => 6,
|
||||
4 => 1,
|
||||
5 => 5,
|
||||
6 => 3,
|
||||
7 => 7,
|
||||
8 => 8,
|
||||
9 => 12,
|
||||
10 => 10,
|
||||
11 => 14,
|
||||
12 => 9,
|
||||
13 => 13,
|
||||
14 => 11,
|
||||
15 => 15,
|
||||
_ => number,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,269 +1,264 @@
|
|||
using System;
|
||||
using Microsoft.Windows.Sdk;
|
||||
using Spectre.Terminals.Emulation;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsTerminalEmulator : IAnsiSequenceVisitor<WindowsTerminalState>
|
||||
{
|
||||
internal sealed class WindowsTerminalEmulator : IAnsiSequenceVisitor<WindowsTerminalState>
|
||||
public void Write(WindowsTerminalState state, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
public void Write(WindowsTerminalState state, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
// TODO: Not very efficient
|
||||
// TODO: Not very efficient
|
||||
#if NET5_0_OR_GREATER
|
||||
var text = state.Encoding.GetString(buffer);
|
||||
var text = state.Encoding.GetString(buffer);
|
||||
#else
|
||||
var text = state.Encoding.GetString(buffer.ToArray());
|
||||
var text = state.Encoding.GetString(buffer.ToArray());
|
||||
#endif
|
||||
AnsiInterpreter.Interpret(this, state, text);
|
||||
}
|
||||
AnsiInterpreter.Interpret(this, state, text);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorUp(CursorUp op, WindowsTerminalState state)
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorUp(CursorUp op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, y: -1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorDown(CursorDown op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, y: 1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorBack(CursorBack op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, x: -1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorForward(CursorForward op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, x: 1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorHorizontalAbsolute(CursorHorizontalAbsolute op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
MoveCursorRelative(state, y: -1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorDown(CursorDown op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, y: 1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorBack(CursorBack op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, x: -1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorForward(CursorForward op, WindowsTerminalState state)
|
||||
{
|
||||
MoveCursorRelative(state, x: 1);
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorHorizontalAbsolute(CursorHorizontalAbsolute op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X = (short)op.Column;
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorNextLine(CursorNextLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X = 0;
|
||||
info.dwCursorPosition.Y += (short)(1 * op.Count);
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorPreviousLine(CursorPreviousLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X = 0;
|
||||
info.dwCursorPosition.Y -= (short)(1 * op.Count);
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorPosition(CursorPosition op, WindowsTerminalState state)
|
||||
{
|
||||
SetCursorPosition(state, new COORD
|
||||
{
|
||||
X = (short)op.Column,
|
||||
Y = (short)op.Row,
|
||||
});
|
||||
}
|
||||
|
||||
unsafe void IAnsiSequenceVisitor<WindowsTerminalState>.EraseInDisplay(EraseInDisplay op, WindowsTerminalState state)
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.Mode == 0)
|
||||
{
|
||||
// Delete everything after the cursor
|
||||
var skip = ((info.dwCursorPosition.Y - 1) * info.dwSize.X) + info.dwCursorPosition.X;
|
||||
var length = (info.dwSize.X * info.dwSize.Y) - skip;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, new COORD()
|
||||
{
|
||||
X = info.dwCursorPosition.X,
|
||||
Y = info.dwCursorPosition.Y,
|
||||
}, out _);
|
||||
}
|
||||
else if (op.Mode == 1)
|
||||
{
|
||||
// Delete everything before the cursor
|
||||
var length = (info.dwCursorPosition.Y * info.dwSize.X) + info.dwCursorPosition.X;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, default(COORD), out _);
|
||||
}
|
||||
else if (op.Mode == 2 || op.Mode == 3)
|
||||
{
|
||||
// Delete everything
|
||||
var terminalSize = info.dwSize.X * info.dwSize.Y;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)terminalSize, default(COORD), out _);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.EraseInLine(EraseInLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.Mode == 0)
|
||||
{
|
||||
// Delete line after the cursor
|
||||
var length = info.dwSize.X - info.dwCursorPosition.X;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, info.dwCursorPosition, out _);
|
||||
}
|
||||
else if (op.Mode == 1)
|
||||
{
|
||||
// Delete line before the cursor
|
||||
var length = info.dwCursorPosition.X;
|
||||
var pos = new COORD { X = 0, Y = info.dwCursorPosition.Y };
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, pos, out _);
|
||||
}
|
||||
else if (op.Mode == 2)
|
||||
{
|
||||
// Delete whole line
|
||||
var pos = new COORD { X = 0, Y = info.dwCursorPosition.Y };
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)info.dwSize.X, pos, out _);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.PrintText(PrintText op, WindowsTerminalState state)
|
||||
{
|
||||
// TODO: Not very efficient
|
||||
var bytes = state.Encoding.GetBytes(op.Text.ToArray());
|
||||
state.Writer.Write(state.Handle, new ReadOnlySpan<byte>(bytes));
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.RestoreCursor(RestoreCursor op, WindowsTerminalState state)
|
||||
{
|
||||
if (state.StoredCursorPosition != null)
|
||||
{
|
||||
SetCursorPosition(state, state.StoredCursorPosition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.StoreCursor(StoreCursor op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
state.StoredCursorPosition = info.dwCursorPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.StoredCursorPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.HideCursor(HideCursor instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleCursorInfo(state.Handle, out var info))
|
||||
{
|
||||
PInvoke.SetConsoleCursorInfo(state.Handle, new CONSOLE_CURSOR_INFO
|
||||
{
|
||||
bVisible = false,
|
||||
dwSize = info.dwSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.ShowCursor(ShowCursor instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleCursorInfo(state.Handle, out var info))
|
||||
{
|
||||
PInvoke.SetConsoleCursorInfo(state.Handle, new CONSOLE_CURSOR_INFO
|
||||
{
|
||||
bVisible = true,
|
||||
dwSize = info.dwSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe void IAnsiSequenceVisitor<WindowsTerminalState>.EnableAlternativeBuffer(
|
||||
EnableAlternativeBuffer instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try creating the screen buffer
|
||||
state.AlternativeBuffer = PInvoke.CreateConsoleScreenBuffer(
|
||||
WindowsConstants.GENERIC_WRITE, WindowsConstants.FILE_SHARE_WRITE,
|
||||
(SECURITY_ATTRIBUTES?)null, WindowsConstants.CONSOLE_TEXTMODE_BUFFER, null);
|
||||
|
||||
// Set the screenbuffer if successful
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
PInvoke.SetConsoleActiveScreenBuffer(state.AlternativeBuffer);
|
||||
SetCursorPosition(state, new COORD
|
||||
{
|
||||
X = 0,
|
||||
Y = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.DisableAlternativeBuffer(
|
||||
DisableAlternativeBuffer instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
if (PInvoke.SetConsoleActiveScreenBuffer(state.MainBuffer))
|
||||
{
|
||||
var handle = state.AlternativeBuffer.DangerousGetHandle();
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
PInvoke.CloseHandle(new HANDLE(handle));
|
||||
state.AlternativeBuffer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.SelectGraphicRendition(
|
||||
SelectGraphicRendition instruction, WindowsTerminalState state)
|
||||
{
|
||||
foreach (var operation in instruction.Operations)
|
||||
{
|
||||
if (operation.Reset)
|
||||
{
|
||||
state.Colors.Reset();
|
||||
}
|
||||
else if (operation.Foreground != null)
|
||||
{
|
||||
state.Colors.SetForeground(operation.Foreground.Value);
|
||||
}
|
||||
else if (operation.Background != null)
|
||||
{
|
||||
state.Colors.SetBackground(operation.Background.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void MoveCursorRelative(WindowsTerminalState state, short x = 0, short y = 0)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X += x;
|
||||
info.dwCursorPosition.Y += y;
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCursorPosition(WindowsTerminalState state, COORD coordinates)
|
||||
{
|
||||
coordinates.X = (short)Math.Max(coordinates.X - 1, 0);
|
||||
coordinates.Y = (short)Math.Max(coordinates.Y - 1, 0);
|
||||
|
||||
PInvoke.SetConsoleCursorPosition(state.Handle, coordinates);
|
||||
info.dwCursorPosition.X = (short)op.Column;
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorNextLine(CursorNextLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X = 0;
|
||||
info.dwCursorPosition.Y += (short)(1 * op.Count);
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorPreviousLine(CursorPreviousLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X = 0;
|
||||
info.dwCursorPosition.Y -= (short)(1 * op.Count);
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.CursorPosition(CursorPosition op, WindowsTerminalState state)
|
||||
{
|
||||
SetCursorPosition(state, new COORD
|
||||
{
|
||||
X = (short)op.Column,
|
||||
Y = (short)op.Row,
|
||||
});
|
||||
}
|
||||
|
||||
unsafe void IAnsiSequenceVisitor<WindowsTerminalState>.EraseInDisplay(EraseInDisplay op, WindowsTerminalState state)
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.Mode == 0)
|
||||
{
|
||||
// Delete everything after the cursor
|
||||
var skip = ((info.dwCursorPosition.Y - 1) * info.dwSize.X) + info.dwCursorPosition.X;
|
||||
var length = (info.dwSize.X * info.dwSize.Y) - skip;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, new COORD()
|
||||
{
|
||||
X = info.dwCursorPosition.X,
|
||||
Y = info.dwCursorPosition.Y,
|
||||
}, out _);
|
||||
}
|
||||
else if (op.Mode == 1)
|
||||
{
|
||||
// Delete everything before the cursor
|
||||
var length = (info.dwCursorPosition.Y * info.dwSize.X) + info.dwCursorPosition.X;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, default(COORD), out _);
|
||||
}
|
||||
else if (op.Mode == 2 || op.Mode == 3)
|
||||
{
|
||||
// Delete everything
|
||||
var terminalSize = info.dwSize.X * info.dwSize.Y;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)terminalSize, default(COORD), out _);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.EraseInLine(EraseInLine op, WindowsTerminalState state)
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.Mode == 0)
|
||||
{
|
||||
// Delete line after the cursor
|
||||
var length = info.dwSize.X - info.dwCursorPosition.X;
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, info.dwCursorPosition, out _);
|
||||
}
|
||||
else if (op.Mode == 1)
|
||||
{
|
||||
// Delete line before the cursor
|
||||
var length = info.dwCursorPosition.X;
|
||||
var pos = new COORD { X = 0, Y = info.dwCursorPosition.Y };
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)length, pos, out _);
|
||||
}
|
||||
else if (op.Mode == 2)
|
||||
{
|
||||
// Delete whole line
|
||||
var pos = new COORD { X = 0, Y = info.dwCursorPosition.Y };
|
||||
PInvoke.FillConsoleOutputCharacter(state.Handle, ' ', (uint)info.dwSize.X, pos, out _);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.PrintText(PrintText op, WindowsTerminalState state)
|
||||
{
|
||||
// TODO: Not very efficient
|
||||
var bytes = state.Encoding.GetBytes(op.Text.ToArray());
|
||||
state.Writer.Write(state.Handle, new ReadOnlySpan<byte>(bytes));
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.RestoreCursor(RestoreCursor op, WindowsTerminalState state)
|
||||
{
|
||||
if (state.StoredCursorPosition != null)
|
||||
{
|
||||
SetCursorPosition(state, state.StoredCursorPosition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.StoreCursor(StoreCursor op, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
state.StoredCursorPosition = info.dwCursorPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.StoredCursorPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.HideCursor(HideCursor instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleCursorInfo(state.Handle, out var info))
|
||||
{
|
||||
PInvoke.SetConsoleCursorInfo(state.Handle, new CONSOLE_CURSOR_INFO
|
||||
{
|
||||
bVisible = false,
|
||||
dwSize = info.dwSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.ShowCursor(ShowCursor instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (PInvoke.GetConsoleCursorInfo(state.Handle, out var info))
|
||||
{
|
||||
PInvoke.SetConsoleCursorInfo(state.Handle, new CONSOLE_CURSOR_INFO
|
||||
{
|
||||
bVisible = true,
|
||||
dwSize = info.dwSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe void IAnsiSequenceVisitor<WindowsTerminalState>.EnableAlternativeBuffer(
|
||||
EnableAlternativeBuffer instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try creating the screen buffer
|
||||
state.AlternativeBuffer = PInvoke.CreateConsoleScreenBuffer(
|
||||
WindowsConstants.GENERIC_WRITE, WindowsConstants.FILE_SHARE_WRITE,
|
||||
(SECURITY_ATTRIBUTES?)null, WindowsConstants.CONSOLE_TEXTMODE_BUFFER, null);
|
||||
|
||||
// Set the screenbuffer if successful
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
PInvoke.SetConsoleActiveScreenBuffer(state.AlternativeBuffer);
|
||||
SetCursorPosition(state, new COORD
|
||||
{
|
||||
X = 0,
|
||||
Y = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.DisableAlternativeBuffer(
|
||||
DisableAlternativeBuffer instruction, WindowsTerminalState state)
|
||||
{
|
||||
if (state.AlternativeBuffer != null)
|
||||
{
|
||||
if (PInvoke.SetConsoleActiveScreenBuffer(state.MainBuffer))
|
||||
{
|
||||
var handle = state.AlternativeBuffer.DangerousGetHandle();
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
PInvoke.CloseHandle(new HANDLE(handle));
|
||||
state.AlternativeBuffer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IAnsiSequenceVisitor<WindowsTerminalState>.SelectGraphicRendition(
|
||||
SelectGraphicRendition instruction, WindowsTerminalState state)
|
||||
{
|
||||
foreach (var operation in instruction.Operations)
|
||||
{
|
||||
if (operation.Reset)
|
||||
{
|
||||
state.Colors.Reset();
|
||||
}
|
||||
else if (operation.Foreground != null)
|
||||
{
|
||||
state.Colors.SetForeground(operation.Foreground.Value);
|
||||
}
|
||||
else if (operation.Background != null)
|
||||
{
|
||||
state.Colors.SetBackground(operation.Background.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void MoveCursorRelative(WindowsTerminalState state, short x = 0, short y = 0)
|
||||
{
|
||||
if (PInvoke.GetConsoleScreenBufferInfo(state.Handle, out var info))
|
||||
{
|
||||
info.dwCursorPosition.X += x;
|
||||
info.dwCursorPosition.Y += y;
|
||||
SetCursorPosition(state, info.dwCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCursorPosition(WindowsTerminalState state, COORD coordinates)
|
||||
{
|
||||
coordinates.X = (short)Math.Max(coordinates.X - 1, 0);
|
||||
coordinates.Y = (short)Math.Max(coordinates.Y - 1, 0);
|
||||
|
||||
PInvoke.SetConsoleCursorPosition(state.Handle, coordinates);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,62 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsTerminalEmulatorAdapter : IWindowsTerminalWriter
|
||||
{
|
||||
internal sealed class WindowsTerminalEmulatorAdapter : IWindowsTerminalWriter
|
||||
private readonly IWindowsTerminalWriter _writer;
|
||||
private readonly WindowsTerminalEmulator _emulator;
|
||||
private readonly WindowsTerminalState _state;
|
||||
|
||||
public SafeHandle Handle => _writer.Handle;
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly IWindowsTerminalWriter _writer;
|
||||
private readonly WindowsTerminalEmulator _emulator;
|
||||
private readonly WindowsTerminalState _state;
|
||||
get => _writer.Encoding;
|
||||
set => _writer.Encoding = value;
|
||||
}
|
||||
|
||||
public SafeHandle Handle => _writer.Handle;
|
||||
public bool IsRedirected => _writer.IsRedirected;
|
||||
|
||||
public Encoding Encoding
|
||||
public WindowsTerminalEmulatorAdapter(IWindowsTerminalWriter writer, WindowsColors colors)
|
||||
{
|
||||
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
_emulator = new WindowsTerminalEmulator();
|
||||
_state = new WindowsTerminalState(writer, colors);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.Dispose();
|
||||
}
|
||||
|
||||
public bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode)
|
||||
{
|
||||
return _writer.GetMode(out mode);
|
||||
}
|
||||
|
||||
public bool AddMode(CONSOLE_MODE mode)
|
||||
{
|
||||
return _writer.AddMode(mode);
|
||||
}
|
||||
|
||||
public bool RemoveMode(CONSOLE_MODE mode)
|
||||
{
|
||||
return _writer.RemoveMode(mode);
|
||||
}
|
||||
|
||||
public void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (_writer.IsRedirected)
|
||||
{
|
||||
get => _writer.Encoding;
|
||||
set => _writer.Encoding = value;
|
||||
_writer.Write(Handle, buffer);
|
||||
}
|
||||
|
||||
public bool IsRedirected => _writer.IsRedirected;
|
||||
|
||||
public WindowsTerminalEmulatorAdapter(IWindowsTerminalWriter writer, WindowsColors colors)
|
||||
else
|
||||
{
|
||||
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
_emulator = new WindowsTerminalEmulator();
|
||||
_state = new WindowsTerminalState(writer, colors);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.Dispose();
|
||||
}
|
||||
|
||||
public bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode)
|
||||
{
|
||||
return _writer.GetMode(out mode);
|
||||
}
|
||||
|
||||
public bool AddMode(CONSOLE_MODE mode)
|
||||
{
|
||||
return _writer.AddMode(mode);
|
||||
}
|
||||
|
||||
public bool RemoveMode(CONSOLE_MODE mode)
|
||||
{
|
||||
return _writer.RemoveMode(mode);
|
||||
}
|
||||
|
||||
public void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (_writer.IsRedirected)
|
||||
{
|
||||
_writer.Write(Handle, buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_emulator.Write(_state, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(SafeHandle handle, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
_emulator.Write(_state, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(SafeHandle handle, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,22 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal class WindowsTerminalState
|
||||
{
|
||||
internal class WindowsTerminalState
|
||||
public IWindowsTerminalWriter Writer { get; }
|
||||
public SafeHandle Handle => AlternativeBuffer ?? MainBuffer;
|
||||
public Encoding Encoding => Writer.Encoding;
|
||||
|
||||
public COORD? StoredCursorPosition { get; set; }
|
||||
|
||||
public SafeHandle MainBuffer { get; set; }
|
||||
public SafeHandle? AlternativeBuffer { get; set; }
|
||||
|
||||
public WindowsColors Colors { get; }
|
||||
|
||||
public WindowsTerminalState(IWindowsTerminalWriter writer, WindowsColors colors)
|
||||
{
|
||||
public IWindowsTerminalWriter Writer { get; }
|
||||
public SafeHandle Handle => AlternativeBuffer ?? MainBuffer;
|
||||
public Encoding Encoding => Writer.Encoding;
|
||||
|
||||
public COORD? StoredCursorPosition { get; set; }
|
||||
|
||||
public SafeHandle MainBuffer { get; set; }
|
||||
public SafeHandle? AlternativeBuffer { get; set; }
|
||||
|
||||
public WindowsColors Colors { get; }
|
||||
|
||||
public WindowsTerminalState(IWindowsTerminalWriter writer, WindowsColors colors)
|
||||
{
|
||||
MainBuffer = writer.Handle;
|
||||
Writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
Colors = colors ?? throw new ArgumentNullException(nameof(colors));
|
||||
}
|
||||
MainBuffer = writer.Handle;
|
||||
Writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
Colors = colors ?? throw new ArgumentNullException(nameof(colors));
|
||||
}
|
||||
}
|
|
@ -1,46 +1,40 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
/// <summary>
|
||||
/// Represents a Windows writer.
|
||||
/// </summary>
|
||||
internal interface IWindowsTerminalWriter : ITerminalWriter, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Windows writer.
|
||||
/// Gets the handle.
|
||||
/// </summary>
|
||||
internal interface IWindowsTerminalWriter : ITerminalWriter, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the handle.
|
||||
/// </summary>
|
||||
SafeHandle Handle { get; }
|
||||
SafeHandle Handle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified data to the specified handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to write to.</param>
|
||||
/// <param name="buffer">The buffer to write.</param>
|
||||
void Write(SafeHandle handle, ReadOnlySpan<byte> buffer);
|
||||
/// <summary>
|
||||
/// Writes the specified data to the specified handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to write to.</param>
|
||||
/// <param name="buffer">The buffer to write.</param>
|
||||
void Write(SafeHandle handle, ReadOnlySpan<byte> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CONSOLE_MODE"/> for the writer.
|
||||
/// </summary>
|
||||
/// <param name="mode">The resulting <see cref="CONSOLE_MODE"/>, or <c>null</c> if the operation failed.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode);
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CONSOLE_MODE"/> for the writer.
|
||||
/// </summary>
|
||||
/// <param name="mode">The resulting <see cref="CONSOLE_MODE"/>, or <c>null</c> if the operation failed.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see cref="CONSOLE_MODE"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="CONSOLE_MODE"/> to add.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool AddMode(CONSOLE_MODE mode);
|
||||
/// <summary>
|
||||
/// Adds the specified <see cref="CONSOLE_MODE"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="CONSOLE_MODE"/> to add.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool AddMode(CONSOLE_MODE mode);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified <see cref="CONSOLE_MODE"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="CONSOLE_MODE"/> to remove.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool RemoveMode(CONSOLE_MODE mode);
|
||||
}
|
||||
/// <summary>
|
||||
/// Removes the specified <see cref="CONSOLE_MODE"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="CONSOLE_MODE"/> to remove.</param>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool RemoveMode(CONSOLE_MODE mode);
|
||||
}
|
||||
|
|
|
@ -1,91 +1,85 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsConsoleStream : Stream
|
||||
{
|
||||
internal sealed class WindowsConsoleStream : Stream
|
||||
private const int BytesPerWChar = 2;
|
||||
|
||||
private readonly SafeHandle _handle;
|
||||
private readonly bool _useFileAPIs;
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
private const int BytesPerWChar = 2;
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
private readonly SafeHandle _handle;
|
||||
private readonly bool _useFileAPIs;
|
||||
public WindowsConsoleStream(SafeHandle handle, bool useFileApis)
|
||||
{
|
||||
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
_useFileAPIs = useFileApis;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return ReadFromHandle(new Span<byte>(buffer, offset, count));
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private unsafe int ReadFromHandle(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
public WindowsConsoleStream(SafeHandle handle, bool useFileApis)
|
||||
{
|
||||
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
_useFileAPIs = useFileApis;
|
||||
}
|
||||
bool readSuccess;
|
||||
var bytesRead = 0;
|
||||
|
||||
public override void Flush()
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return ReadFromHandle(new Span<byte>(buffer, offset, count));
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private unsafe int ReadFromHandle(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
if (_useFileAPIs)
|
||||
{
|
||||
return 0;
|
||||
uint result;
|
||||
var ptrResult = &result;
|
||||
readSuccess = PInvoke.ReadFile(_handle, p, (uint)buffer.Length, ptrResult, null);
|
||||
bytesRead = (int)result;
|
||||
}
|
||||
|
||||
bool readSuccess;
|
||||
var bytesRead = 0;
|
||||
|
||||
fixed (byte* p = buffer)
|
||||
else
|
||||
{
|
||||
if (_useFileAPIs)
|
||||
{
|
||||
uint result;
|
||||
var ptrResult = &result;
|
||||
readSuccess = PInvoke.ReadFile(_handle, p, (uint)buffer.Length, ptrResult, null);
|
||||
bytesRead = (int)result;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint result;
|
||||
var ptrResult = &result;
|
||||
readSuccess = PInvoke.ReadConsole(_handle, p, (uint)buffer.Length, out var charsRead, null);
|
||||
bytesRead = (int)(charsRead * BytesPerWChar);
|
||||
}
|
||||
uint result;
|
||||
var ptrResult = &result;
|
||||
readSuccess = PInvoke.ReadConsole(_handle, p, (uint)buffer.Length, out var charsRead, null);
|
||||
bytesRead = (int)(charsRead * BytesPerWChar);
|
||||
}
|
||||
|
||||
if (readSuccess)
|
||||
{
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not read from console input");
|
||||
}
|
||||
|
||||
if (readSuccess)
|
||||
{
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not read from console input");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,32 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
internal static class WindowsConstants
|
||||
{
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
internal static class WindowsConstants
|
||||
public const int ERROR_HANDLE_EOF = 38;
|
||||
public const int ERROR_BROKEN_PIPE = 109;
|
||||
public const int ERROR_NO_DATA = 232;
|
||||
|
||||
public const int KEY_EVENT = 0x0001;
|
||||
|
||||
public const uint GENERIC_READ = 0x80000000;
|
||||
public const uint GENERIC_WRITE = 0x40000000;
|
||||
|
||||
public const int FILE_SHARE_READ = 1;
|
||||
public const int FILE_SHARE_WRITE = 2;
|
||||
|
||||
public const uint CONSOLE_TEXTMODE_BUFFER = 1;
|
||||
|
||||
public static class Signals
|
||||
{
|
||||
public const int ERROR_HANDLE_EOF = 38;
|
||||
public const int ERROR_BROKEN_PIPE = 109;
|
||||
public const int ERROR_NO_DATA = 232;
|
||||
public const uint CTRL_C_EVENT = 0;
|
||||
public const uint CTRL_BREAK_EVENT = 1;
|
||||
}
|
||||
|
||||
public const int KEY_EVENT = 0x0001;
|
||||
|
||||
public const uint GENERIC_READ = 0x80000000;
|
||||
public const uint GENERIC_WRITE = 0x40000000;
|
||||
|
||||
public const int FILE_SHARE_READ = 1;
|
||||
public const int FILE_SHARE_WRITE = 2;
|
||||
|
||||
public const uint CONSOLE_TEXTMODE_BUFFER = 1;
|
||||
|
||||
public static class Signals
|
||||
{
|
||||
public const uint CTRL_C_EVENT = 0;
|
||||
public const uint CTRL_BREAK_EVENT = 1;
|
||||
}
|
||||
|
||||
public static class Colors
|
||||
{
|
||||
public const short FOREGROUND_MASK = 0xf;
|
||||
public const short BACKGROUND_MASK = 0xf0;
|
||||
public const short COLOR_MASK = 0xff;
|
||||
}
|
||||
public static class Colors
|
||||
{
|
||||
public const short FOREGROUND_MASK = 0xf;
|
||||
public const short BACKGROUND_MASK = 0xf0;
|
||||
public const short COLOR_MASK = 0xff;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,132 +1,127 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsDriver : ITerminalDriver
|
||||
{
|
||||
internal sealed class WindowsDriver : ITerminalDriver
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
private const CONSOLE_MODE IN_MODE = CONSOLE_MODE.ENABLE_PROCESSED_INPUT | CONSOLE_MODE.ENABLE_LINE_INPUT | CONSOLE_MODE.ENABLE_ECHO_INPUT;
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
private const CONSOLE_MODE OUT_MODE = CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN;
|
||||
|
||||
private readonly WindowsTerminalReader _input;
|
||||
private readonly IWindowsTerminalWriter _output;
|
||||
private readonly IWindowsTerminalWriter _error;
|
||||
private readonly WindowsSignals _signals;
|
||||
|
||||
public string Name { get; } = "Windows";
|
||||
public bool IsRawMode { get; private set; }
|
||||
public TerminalSize? Size => GetTerminalSize();
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
{
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
private const CONSOLE_MODE IN_MODE = CONSOLE_MODE.ENABLE_PROCESSED_INPUT | CONSOLE_MODE.ENABLE_LINE_INPUT | CONSOLE_MODE.ENABLE_ECHO_INPUT;
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
|
||||
private const CONSOLE_MODE OUT_MODE = CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN;
|
||||
add => _signals.Signalled += value;
|
||||
remove => _signals.Signalled += value;
|
||||
}
|
||||
|
||||
private readonly WindowsTerminalReader _input;
|
||||
private readonly IWindowsTerminalWriter _output;
|
||||
private readonly IWindowsTerminalWriter _error;
|
||||
private readonly WindowsSignals _signals;
|
||||
ITerminalReader ITerminalDriver.Input => _input;
|
||||
ITerminalWriter ITerminalDriver.Output => _output;
|
||||
ITerminalWriter ITerminalDriver.Error => _error;
|
||||
|
||||
public string Name { get; } = "Windows";
|
||||
public bool IsRawMode { get; private set; }
|
||||
public TerminalSize? Size => GetTerminalSize();
|
||||
public WindowsDriver(bool emulate = false)
|
||||
{
|
||||
_signals = new WindowsSignals();
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
_input = new WindowsTerminalReader(this);
|
||||
_input.AddMode(
|
||||
CONSOLE_MODE.ENABLE_LINE_INPUT |
|
||||
CONSOLE_MODE.ENABLE_ECHO_INPUT |
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_INPUT |
|
||||
CONSOLE_MODE.ENABLE_INSERT_MODE);
|
||||
|
||||
_output = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_OUTPUT_HANDLE);
|
||||
_output.AddMode(
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT |
|
||||
CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT |
|
||||
CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN |
|
||||
CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
|
||||
_error = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_ERROR_HANDLE);
|
||||
_error.AddMode(
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT |
|
||||
CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT |
|
||||
CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN |
|
||||
CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
|
||||
// ENABLE_VIRTUAL_TERMINAL_PROCESSING not supported?
|
||||
if (emulate || (!(_output.GetMode(out var mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0)
|
||||
&& !(_error.GetMode(out mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0)))
|
||||
{
|
||||
add => _signals.Signalled += value;
|
||||
remove => _signals.Signalled += value;
|
||||
}
|
||||
var colors = new WindowsColors(_output.Handle, _error.Handle);
|
||||
|
||||
ITerminalReader ITerminalDriver.Input => _input;
|
||||
ITerminalWriter ITerminalDriver.Output => _output;
|
||||
ITerminalWriter ITerminalDriver.Error => _error;
|
||||
// Wrap STDOUT and STDERR in an emulator
|
||||
_output = new WindowsTerminalEmulatorAdapter(_output, colors);
|
||||
_error = new WindowsTerminalEmulatorAdapter(_error, colors);
|
||||
|
||||
public WindowsDriver(bool emulate = false)
|
||||
{
|
||||
_signals = new WindowsSignals();
|
||||
|
||||
_input = new WindowsTerminalReader(this);
|
||||
_input.AddMode(
|
||||
CONSOLE_MODE.ENABLE_LINE_INPUT |
|
||||
CONSOLE_MODE.ENABLE_ECHO_INPUT |
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_INPUT |
|
||||
CONSOLE_MODE.ENABLE_INSERT_MODE);
|
||||
|
||||
_output = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_OUTPUT_HANDLE);
|
||||
_output.AddMode(
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT |
|
||||
CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT |
|
||||
CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN |
|
||||
CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
|
||||
_error = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_ERROR_HANDLE);
|
||||
_error.AddMode(
|
||||
CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT |
|
||||
CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT |
|
||||
CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN |
|
||||
CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
|
||||
// ENABLE_VIRTUAL_TERMINAL_PROCESSING not supported?
|
||||
if (emulate || (!(_output.GetMode(out var mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0)
|
||||
&& !(_error.GetMode(out mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0)))
|
||||
{
|
||||
var colors = new WindowsColors(_output.Handle, _error.Handle);
|
||||
|
||||
// Wrap STDOUT and STDERR in an emulator
|
||||
_output = new WindowsTerminalEmulatorAdapter(_output, colors);
|
||||
_error = new WindowsTerminalEmulatorAdapter(_error, colors);
|
||||
|
||||
// Update the name to reflect the changes
|
||||
Name = "Windows (emulated)";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_input.Dispose();
|
||||
_output.Dispose();
|
||||
_error.Dispose();
|
||||
_signals.Dispose();
|
||||
}
|
||||
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
return WindowsSignals.Emit(signal);
|
||||
}
|
||||
|
||||
public bool EnableRawMode()
|
||||
{
|
||||
// TODO: Restoration of mode when failed
|
||||
if (!(_input.RemoveMode(IN_MODE) && (_output.RemoveMode(OUT_MODE) || _error.RemoveMode(OUT_MODE))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PInvoke.FlushConsoleInputBuffer(_input.Handle))
|
||||
{
|
||||
throw new InvalidOperationException("Could not flush input buffer");
|
||||
}
|
||||
|
||||
IsRawMode = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DisableRawMode()
|
||||
{
|
||||
// TODO: Restoration of mode when failed
|
||||
if (!(_input.AddMode(IN_MODE) && (_output.AddMode(OUT_MODE) || _error.AddMode(OUT_MODE))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PInvoke.FlushConsoleInputBuffer(_input.Handle))
|
||||
{
|
||||
throw new InvalidOperationException("Could not flush input buffer");
|
||||
}
|
||||
|
||||
IsRawMode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private TerminalSize? GetTerminalSize()
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(_output.Handle, out var info) &&
|
||||
!PInvoke.GetConsoleScreenBufferInfo(_error.Handle, out info) &&
|
||||
!PInvoke.GetConsoleScreenBufferInfo(_input.Handle, out info))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TerminalSize(info.srWindow.Right, info.srWindow.Bottom);
|
||||
// Update the name to reflect the changes
|
||||
Name = "Windows (emulated)";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_input.Dispose();
|
||||
_output.Dispose();
|
||||
_error.Dispose();
|
||||
_signals.Dispose();
|
||||
}
|
||||
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
return WindowsSignals.Emit(signal);
|
||||
}
|
||||
|
||||
public bool EnableRawMode()
|
||||
{
|
||||
// TODO: Restoration of mode when failed
|
||||
if (!(_input.RemoveMode(IN_MODE) && (_output.RemoveMode(OUT_MODE) || _error.RemoveMode(OUT_MODE))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PInvoke.FlushConsoleInputBuffer(_input.Handle))
|
||||
{
|
||||
throw new InvalidOperationException("Could not flush input buffer");
|
||||
}
|
||||
|
||||
IsRawMode = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DisableRawMode()
|
||||
{
|
||||
// TODO: Restoration of mode when failed
|
||||
if (!(_input.AddMode(IN_MODE) && (_output.AddMode(OUT_MODE) || _error.AddMode(OUT_MODE))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PInvoke.FlushConsoleInputBuffer(_input.Handle))
|
||||
{
|
||||
throw new InvalidOperationException("Could not flush input buffer");
|
||||
}
|
||||
|
||||
IsRawMode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private TerminalSize? GetTerminalSize()
|
||||
{
|
||||
if (!PInvoke.GetConsoleScreenBufferInfo(_output.Handle, out var info) &&
|
||||
!PInvoke.GetConsoleScreenBufferInfo(_error.Handle, out info) &&
|
||||
!PInvoke.GetConsoleScreenBufferInfo(_input.Handle, out info))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TerminalSize(info.srWindow.Right, info.srWindow.Bottom);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,208 +1,204 @@
|
|||
// Parts of this code used from: https://github.com/dotnet/runtime
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
using static Spectre.Terminals.Drivers.WindowsConstants;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
internal sealed class WindowsKeyReader
|
||||
{
|
||||
internal sealed class WindowsKeyReader
|
||||
private const short AltVKCode = 0x12;
|
||||
|
||||
private readonly object _lock;
|
||||
private readonly SafeHandle _handle;
|
||||
private INPUT_RECORD _cachedInputRecord;
|
||||
|
||||
[Flags]
|
||||
private enum ControlKeyState
|
||||
{
|
||||
private const short AltVKCode = 0x12;
|
||||
Unknown = 0,
|
||||
RightAltPressed = 0x0001,
|
||||
LeftAltPressed = 0x0002,
|
||||
RightCtrlPressed = 0x0004,
|
||||
LeftCtrlPressed = 0x0008,
|
||||
ShiftPressed = 0x0010,
|
||||
NumLockOn = 0x0020,
|
||||
ScrollLockOn = 0x0040,
|
||||
CapsLockOn = 0x0080,
|
||||
EnhancedKey = 0x0100,
|
||||
}
|
||||
|
||||
private readonly object _lock;
|
||||
private readonly SafeHandle _handle;
|
||||
private INPUT_RECORD _cachedInputRecord;
|
||||
public WindowsKeyReader(SafeHandle handle)
|
||||
{
|
||||
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum ControlKeyState
|
||||
public unsafe bool IsKeyAvailable()
|
||||
{
|
||||
if (_cachedInputRecord.EventType == KEY_EVENT)
|
||||
{
|
||||
Unknown = 0,
|
||||
RightAltPressed = 0x0001,
|
||||
LeftAltPressed = 0x0002,
|
||||
RightCtrlPressed = 0x0004,
|
||||
LeftCtrlPressed = 0x0008,
|
||||
ShiftPressed = 0x0010,
|
||||
NumLockOn = 0x0020,
|
||||
ScrollLockOn = 0x0040,
|
||||
CapsLockOn = 0x0080,
|
||||
EnhancedKey = 0x0100,
|
||||
return true;
|
||||
}
|
||||
|
||||
public WindowsKeyReader(SafeHandle handle)
|
||||
{
|
||||
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
_lock = new object();
|
||||
}
|
||||
INPUT_RECORD ir;
|
||||
var buffer = new Span<INPUT_RECORD>(new INPUT_RECORD[1]);
|
||||
|
||||
public unsafe bool IsKeyAvailable()
|
||||
while (true)
|
||||
{
|
||||
if (_cachedInputRecord.EventType == KEY_EVENT)
|
||||
var r = PInvoke.PeekConsoleInput(_handle, buffer, out var numEventsRead);
|
||||
if (!r)
|
||||
{
|
||||
return true;
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
INPUT_RECORD ir;
|
||||
var buffer = new Span<INPUT_RECORD>(new INPUT_RECORD[1]);
|
||||
|
||||
while (true)
|
||||
if (numEventsRead == 0)
|
||||
{
|
||||
var r = PInvoke.PeekConsoleInput(_handle, buffer, out var numEventsRead);
|
||||
return false;
|
||||
}
|
||||
|
||||
ir = buffer[0];
|
||||
|
||||
// Skip non key-down && mod key events.
|
||||
if (!IsKeyDownEvent(ir) || IsModKey(ir))
|
||||
{
|
||||
r = PInvoke.ReadConsoleInput(_handle, buffer, out _);
|
||||
if (!r)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (numEventsRead == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ir = buffer[0];
|
||||
|
||||
// Skip non key-down && mod key events.
|
||||
if (!IsKeyDownEvent(ir) || IsModKey(ir))
|
||||
{
|
||||
r = PInvoke.ReadConsoleInput(_handle, buffer, out _);
|
||||
if (!r)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
INPUT_RECORD ir;
|
||||
var buffer = new Span<INPUT_RECORD>(new INPUT_RECORD[1]);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
INPUT_RECORD ir;
|
||||
var buffer = new Span<INPUT_RECORD>(new INPUT_RECORD[1]);
|
||||
|
||||
lock (_lock)
|
||||
if (_cachedInputRecord.EventType == KEY_EVENT)
|
||||
{
|
||||
if (_cachedInputRecord.EventType == KEY_EVENT)
|
||||
// We had a previous keystroke with repeated characters.
|
||||
ir = _cachedInputRecord;
|
||||
if (_cachedInputRecord.Event.KeyEvent.wRepeatCount == 0)
|
||||
{
|
||||
// We had a previous keystroke with repeated characters.
|
||||
ir = _cachedInputRecord;
|
||||
if (_cachedInputRecord.Event.KeyEvent.wRepeatCount == 0)
|
||||
{
|
||||
_cachedInputRecord.EventType = ushort.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cachedInputRecord.Event.KeyEvent.wRepeatCount--;
|
||||
}
|
||||
|
||||
// We will return one key from this method, so we decrement the
|
||||
// repeatCount here, leaving the cachedInputRecord in the "queue".
|
||||
_cachedInputRecord.EventType = ushort.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We did NOT have a previous keystroke with repeated characters:
|
||||
while (true)
|
||||
_cachedInputRecord.Event.KeyEvent.wRepeatCount--;
|
||||
}
|
||||
|
||||
// We will return one key from this method, so we decrement the
|
||||
// repeatCount here, leaving the cachedInputRecord in the "queue".
|
||||
}
|
||||
else
|
||||
{
|
||||
// We did NOT have a previous keystroke with repeated characters:
|
||||
while (true)
|
||||
{
|
||||
var r = PInvoke.ReadConsoleInput(_handle, buffer, out var numEventsRead);
|
||||
if (!r || numEventsRead != 1)
|
||||
{
|
||||
var r = PInvoke.ReadConsoleInput(_handle, buffer, out var numEventsRead);
|
||||
if (!r || numEventsRead != 1)
|
||||
{
|
||||
// This will fail when stdin is redirected from a file or pipe.
|
||||
// We could theoretically call Console.Read here, but I
|
||||
// think we might do some things incorrectly then.
|
||||
throw new InvalidOperationException("Could not read from STDIN. Has it been redirected?");
|
||||
}
|
||||
// This will fail when stdin is redirected from a file or pipe.
|
||||
// We could theoretically call Console.Read here, but I
|
||||
// think we might do some things incorrectly then.
|
||||
throw new InvalidOperationException("Could not read from STDIN. Has it been redirected?");
|
||||
}
|
||||
|
||||
ir = buffer[0];
|
||||
var keyCode = ir.Event.KeyEvent.wVirtualKeyCode;
|
||||
ir = buffer[0];
|
||||
var keyCode = ir.Event.KeyEvent.wVirtualKeyCode;
|
||||
|
||||
// First check for non-keyboard events & discard them. Generally we tap into only KeyDown events and ignore the KeyUp events
|
||||
// but it is possible that we are dealing with a Alt+NumPad unicode key sequence, the final unicode char is revealed only when
|
||||
// the Alt key is released (i.e when the sequence is complete). To avoid noise, when the Alt key is down, we should eat up
|
||||
// any intermediate key strokes (from NumPad) that collectively forms the Unicode character.
|
||||
if (!IsKeyDownEvent(ir))
|
||||
{
|
||||
// REVIEW: Unicode IME input comes through as KeyUp event with no accompanying KeyDown.
|
||||
if (keyCode != AltVKCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var ch = (char)ir.Event.KeyEvent.uChar.UnicodeChar;
|
||||
|
||||
// In a Alt+NumPad unicode sequence, when the alt key is released uChar will represent the final unicode character, we need to
|
||||
// surface this. VirtualKeyCode for this event will be Alt from the Alt-Up key event. This is probably not the right code,
|
||||
// especially when we don't expose ConsoleKey.Alt, so this will end up being the hex value (0x12). VK_PACKET comes very
|
||||
// close to being useful and something that we could look into using for this purpose...
|
||||
if (ch == 0)
|
||||
{
|
||||
// Skip mod keys.
|
||||
if (IsModKey(ir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// When Alt is down, it is possible that we are in the middle of a Alt+NumPad unicode sequence.
|
||||
// Escape any intermediate NumPad keys whether NumLock is on or not (notepad behavior)
|
||||
var key = (ConsoleKey)keyCode;
|
||||
if (IsAltKeyDown(ir) && ((key >= ConsoleKey.NumPad0 && key <= ConsoleKey.NumPad9)
|
||||
|| (key == ConsoleKey.Clear) || (key == ConsoleKey.Insert)
|
||||
|| (key >= ConsoleKey.PageUp && key <= ConsoleKey.DownArrow)))
|
||||
// First check for non-keyboard events & discard them. Generally we tap into only KeyDown events and ignore the KeyUp events
|
||||
// but it is possible that we are dealing with a Alt+NumPad unicode key sequence, the final unicode char is revealed only when
|
||||
// the Alt key is released (i.e when the sequence is complete). To avoid noise, when the Alt key is down, we should eat up
|
||||
// any intermediate key strokes (from NumPad) that collectively forms the Unicode character.
|
||||
if (!IsKeyDownEvent(ir))
|
||||
{
|
||||
// REVIEW: Unicode IME input comes through as KeyUp event with no accompanying KeyDown.
|
||||
if (keyCode != AltVKCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ir.Event.KeyEvent.wRepeatCount > 1)
|
||||
{
|
||||
ir.Event.KeyEvent.wRepeatCount--;
|
||||
_cachedInputRecord = ir;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// we did NOT have a previous keystroke with repeated characters.
|
||||
var ch = (char)ir.Event.KeyEvent.uChar.UnicodeChar;
|
||||
|
||||
// In a Alt+NumPad unicode sequence, when the alt key is released uChar will represent the final unicode character, we need to
|
||||
// surface this. VirtualKeyCode for this event will be Alt from the Alt-Up key event. This is probably not the right code,
|
||||
// especially when we don't expose ConsoleKey.Alt, so this will end up being the hex value (0x12). VK_PACKET comes very
|
||||
// close to being useful and something that we could look into using for this purpose...
|
||||
if (ch == 0)
|
||||
{
|
||||
// Skip mod keys.
|
||||
if (IsModKey(ir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// When Alt is down, it is possible that we are in the middle of a Alt+NumPad unicode sequence.
|
||||
// Escape any intermediate NumPad keys whether NumLock is on or not (notepad behavior)
|
||||
var key = (ConsoleKey)keyCode;
|
||||
if (IsAltKeyDown(ir) && ((key >= ConsoleKey.NumPad0 && key <= ConsoleKey.NumPad9)
|
||||
|| (key == ConsoleKey.Clear) || (key == ConsoleKey.Insert)
|
||||
|| (key >= ConsoleKey.PageUp && key <= ConsoleKey.DownArrow)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ir.Event.KeyEvent.wRepeatCount > 1)
|
||||
{
|
||||
ir.Event.KeyEvent.wRepeatCount--;
|
||||
_cachedInputRecord = ir;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var state = (ControlKeyState)ir.Event.KeyEvent.dwControlKeyState;
|
||||
var shift = (state & ControlKeyState.ShiftPressed) != 0;
|
||||
var alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
|
||||
var control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0;
|
||||
|
||||
return new ConsoleKeyInfo(
|
||||
(char)ir.Event.KeyEvent.uChar.UnicodeChar,
|
||||
(ConsoleKey)ir.Event.KeyEvent.wVirtualKeyCode,
|
||||
shift, alt, control);
|
||||
// we did NOT have a previous keystroke with repeated characters.
|
||||
}
|
||||
|
||||
private static bool IsKeyDownEvent(INPUT_RECORD ir)
|
||||
{
|
||||
return ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown.Value != 0;
|
||||
}
|
||||
var state = (ControlKeyState)ir.Event.KeyEvent.dwControlKeyState;
|
||||
var shift = (state & ControlKeyState.ShiftPressed) != 0;
|
||||
var alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
|
||||
var control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0;
|
||||
|
||||
private static bool IsModKey(INPUT_RECORD ir)
|
||||
{
|
||||
// We should also skip over Shift, Control, and Alt, as well as caps lock.
|
||||
// Apparently we don't need to check for 0xA0 through 0xA5, which are keys like
|
||||
// Left Control & Right Control. See the ConsoleKey enum for these values.
|
||||
var keyCode = ir.Event.KeyEvent.wVirtualKeyCode;
|
||||
return (keyCode >= 0x10 && keyCode <= 0x12) || keyCode == 0x14 || keyCode == 0x90 || keyCode == 0x91;
|
||||
}
|
||||
return new ConsoleKeyInfo(
|
||||
(char)ir.Event.KeyEvent.uChar.UnicodeChar,
|
||||
(ConsoleKey)ir.Event.KeyEvent.wVirtualKeyCode,
|
||||
shift, alt, control);
|
||||
}
|
||||
|
||||
// For tracking Alt+NumPad unicode key sequence. When you press Alt key down
|
||||
// and press a numpad unicode decimal sequence and then release Alt key, the
|
||||
// desired effect is to translate the sequence into one Unicode KeyPress.
|
||||
// We need to keep track of the Alt+NumPad sequence and surface the final
|
||||
// unicode char alone when the Alt key is released.
|
||||
private static bool IsAltKeyDown(INPUT_RECORD ir)
|
||||
{
|
||||
return (((ControlKeyState)ir.Event.KeyEvent.dwControlKeyState)
|
||||
& (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
|
||||
}
|
||||
private static bool IsKeyDownEvent(INPUT_RECORD ir)
|
||||
{
|
||||
return ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown.Value != 0;
|
||||
}
|
||||
|
||||
private static bool IsModKey(INPUT_RECORD ir)
|
||||
{
|
||||
// We should also skip over Shift, Control, and Alt, as well as caps lock.
|
||||
// Apparently we don't need to check for 0xA0 through 0xA5, which are keys like
|
||||
// Left Control & Right Control. See the ConsoleKey enum for these values.
|
||||
var keyCode = ir.Event.KeyEvent.wVirtualKeyCode;
|
||||
return (keyCode >= 0x10 && keyCode <= 0x12) || keyCode == 0x14 || keyCode == 0x90 || keyCode == 0x91;
|
||||
}
|
||||
|
||||
// For tracking Alt+NumPad unicode key sequence. When you press Alt key down
|
||||
// and press a numpad unicode decimal sequence and then release Alt key, the
|
||||
// desired effect is to translate the sequence into one Unicode KeyPress.
|
||||
// We need to keep track of the Alt+NumPad sequence and surface the final
|
||||
// unicode char alone when the Alt key is released.
|
||||
private static bool IsAltKeyDown(INPUT_RECORD ir)
|
||||
{
|
||||
return (((ControlKeyState)ir.Event.KeyEvent.dwControlKeyState)
|
||||
& (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
|
||||
}
|
||||
}
|
|
@ -1,115 +1,110 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsSignals : IDisposable
|
||||
{
|
||||
internal sealed class WindowsSignals : IDisposable
|
||||
private readonly object _lock;
|
||||
private bool _installed;
|
||||
|
||||
private EventHandler<TerminalSignalEventArgs>? _event;
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
{
|
||||
private readonly object _lock;
|
||||
private bool _installed;
|
||||
|
||||
private EventHandler<TerminalSignalEventArgs>? _event;
|
||||
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
add
|
||||
{
|
||||
add
|
||||
{
|
||||
_event = value;
|
||||
InstallHandler();
|
||||
}
|
||||
remove
|
||||
{
|
||||
UninstallHandler();
|
||||
_event = null;
|
||||
}
|
||||
_event = value;
|
||||
InstallHandler();
|
||||
}
|
||||
|
||||
public WindowsSignals()
|
||||
{
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
remove
|
||||
{
|
||||
UninstallHandler();
|
||||
_event = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Emit(TerminalSignal signal)
|
||||
public WindowsSignals()
|
||||
{
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UninstallHandler();
|
||||
}
|
||||
|
||||
public static bool Emit(TerminalSignal signal)
|
||||
{
|
||||
uint? ctrlEvent = signal switch
|
||||
{
|
||||
uint? ctrlEvent = signal switch
|
||||
{
|
||||
TerminalSignal.SIGINT => WindowsConstants.Signals.CTRL_C_EVENT,
|
||||
TerminalSignal.SIGQUIT => WindowsConstants.Signals.CTRL_C_EVENT,
|
||||
_ => null,
|
||||
};
|
||||
TerminalSignal.SIGINT => WindowsConstants.Signals.CTRL_C_EVENT,
|
||||
TerminalSignal.SIGQUIT => WindowsConstants.Signals.CTRL_C_EVENT,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (ctrlEvent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return PInvoke.GenerateConsoleCtrlEvent(ctrlEvent.Value, 0);
|
||||
}
|
||||
|
||||
private BOOL Callback(uint ctrlType)
|
||||
if (ctrlEvent == null)
|
||||
{
|
||||
var @event = _event;
|
||||
|
||||
TerminalSignal? signal = null;
|
||||
switch (ctrlType)
|
||||
{
|
||||
case WindowsConstants.Signals.CTRL_C_EVENT:
|
||||
signal = TerminalSignal.SIGINT;
|
||||
break;
|
||||
case WindowsConstants.Signals.CTRL_BREAK_EVENT:
|
||||
signal = TerminalSignal.SIGQUIT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (@event != null && signal != null)
|
||||
{
|
||||
var args = new TerminalSignalEventArgs(signal.Value);
|
||||
@event(null, args);
|
||||
return args.Cancel;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void InstallHandler()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_installed)
|
||||
{
|
||||
if (!PInvoke.SetConsoleCtrlHandler(Callback, true))
|
||||
{
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException(
|
||||
$"An error occured when installing Windows signal handler. Error code: {errorCode}");
|
||||
}
|
||||
return PInvoke.GenerateConsoleCtrlEvent(ctrlEvent.Value, 0);
|
||||
}
|
||||
|
||||
_installed = true;
|
||||
}
|
||||
}
|
||||
private BOOL Callback(uint ctrlType)
|
||||
{
|
||||
var @event = _event;
|
||||
|
||||
TerminalSignal? signal = null;
|
||||
switch (ctrlType)
|
||||
{
|
||||
case WindowsConstants.Signals.CTRL_C_EVENT:
|
||||
signal = TerminalSignal.SIGINT;
|
||||
break;
|
||||
case WindowsConstants.Signals.CTRL_BREAK_EVENT:
|
||||
signal = TerminalSignal.SIGQUIT;
|
||||
break;
|
||||
}
|
||||
|
||||
private void UninstallHandler()
|
||||
if (@event != null && signal != null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_installed)
|
||||
{
|
||||
if (!PInvoke.SetConsoleCtrlHandler(Callback, false))
|
||||
{
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException(
|
||||
$"An error occured when uninstalling Windows signal handler. Error code: {errorCode}");
|
||||
}
|
||||
var args = new TerminalSignalEventArgs(signal.Value);
|
||||
@event(null, args);
|
||||
return args.Cancel;
|
||||
}
|
||||
|
||||
_installed = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void InstallHandler()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_installed)
|
||||
{
|
||||
if (!PInvoke.SetConsoleCtrlHandler(Callback, true))
|
||||
{
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException(
|
||||
$"An error occured when installing Windows signal handler. Error code: {errorCode}");
|
||||
}
|
||||
|
||||
_installed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UninstallHandler()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_installed)
|
||||
{
|
||||
if (!PInvoke.SetConsoleCtrlHandler(Callback, false))
|
||||
{
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException(
|
||||
$"An error occured when uninstalling Windows signal handler. Error code: {errorCode}");
|
||||
}
|
||||
|
||||
_installed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,66 +1,60 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal abstract class WindowsTerminalHandle : IDisposable
|
||||
{
|
||||
internal abstract class WindowsTerminalHandle : IDisposable
|
||||
private readonly object _lock;
|
||||
|
||||
public SafeHandle Handle { get; set; }
|
||||
public bool IsRedirected { get; }
|
||||
|
||||
protected WindowsTerminalHandle(STD_HANDLE_TYPE handle)
|
||||
{
|
||||
private readonly object _lock;
|
||||
_lock = new object();
|
||||
|
||||
public SafeHandle Handle { get; set; }
|
||||
public bool IsRedirected { get; }
|
||||
Handle = PInvoke.GetStdHandle_SafeHandle(handle);
|
||||
IsRedirected = !GetMode(out _) || (PInvoke.GetFileType(Handle) & 2) == 0;
|
||||
}
|
||||
|
||||
protected WindowsTerminalHandle(STD_HANDLE_TYPE handle)
|
||||
public void Dispose()
|
||||
{
|
||||
Handle.Dispose();
|
||||
}
|
||||
|
||||
public unsafe bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode)
|
||||
{
|
||||
if (PInvoke.GetConsoleMode(Handle, out var result))
|
||||
{
|
||||
_lock = new object();
|
||||
|
||||
Handle = PInvoke.GetStdHandle_SafeHandle(handle);
|
||||
IsRedirected = !GetMode(out _) || (PInvoke.GetFileType(Handle) & 2) == 0;
|
||||
mode = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Handle.Dispose();
|
||||
}
|
||||
mode = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode)
|
||||
public unsafe bool AddMode(CONSOLE_MODE mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (PInvoke.GetConsoleMode(Handle, out var result))
|
||||
if (GetMode(out var currentMode))
|
||||
{
|
||||
mode = result;
|
||||
return true;
|
||||
return PInvoke.SetConsoleMode(Handle, currentMode.Value | mode);
|
||||
}
|
||||
|
||||
mode = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool AddMode(CONSOLE_MODE mode)
|
||||
public unsafe bool RemoveMode(CONSOLE_MODE mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock)
|
||||
if (GetMode(out var currentMode))
|
||||
{
|
||||
if (GetMode(out var currentMode))
|
||||
{
|
||||
return PInvoke.SetConsoleMode(Handle, currentMode.Value | mode);
|
||||
}
|
||||
|
||||
return false;
|
||||
return PInvoke.SetConsoleMode(Handle, currentMode.Value & ~mode);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool RemoveMode(CONSOLE_MODE mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (GetMode(out var currentMode))
|
||||
{
|
||||
return PInvoke.SetConsoleMode(Handle, currentMode.Value & ~mode);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +1,82 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsTerminalReader : WindowsTerminalHandle, ITerminalReader
|
||||
{
|
||||
internal sealed class WindowsTerminalReader : WindowsTerminalHandle, ITerminalReader
|
||||
private readonly WindowsDriver _driver;
|
||||
private readonly WindowsKeyReader _keyReader;
|
||||
private readonly SynchronizedTextReader _reader;
|
||||
private Encoding _encoding;
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly WindowsDriver _driver;
|
||||
private readonly WindowsKeyReader _keyReader;
|
||||
private readonly SynchronizedTextReader _reader;
|
||||
private Encoding _encoding;
|
||||
get => _encoding;
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
public Encoding Encoding
|
||||
public bool IsRawMode => _driver.IsRawMode;
|
||||
public bool IsKeyAvailable => _keyReader.IsKeyAvailable();
|
||||
|
||||
public WindowsTerminalReader(WindowsDriver driver)
|
||||
: base(STD_HANDLE_TYPE.STD_INPUT_HANDLE)
|
||||
{
|
||||
_encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleCP());
|
||||
_driver = driver;
|
||||
_keyReader = new WindowsKeyReader(Handle);
|
||||
_reader = CreateReader(Handle, _encoding, IsRedirected);
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
return _reader.Read();
|
||||
}
|
||||
|
||||
public string? ReadLine()
|
||||
{
|
||||
return _reader.ReadLine();
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
return _keyReader.ReadKey();
|
||||
}
|
||||
|
||||
private static SynchronizedTextReader CreateReader(SafeHandle handle, Encoding encoding, bool isRedirected)
|
||||
{
|
||||
static Stream Create(SafeHandle handle, bool useFileApis)
|
||||
{
|
||||
get => _encoding;
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
public bool IsRawMode => _driver.IsRawMode;
|
||||
public bool IsKeyAvailable => _keyReader.IsKeyAvailable();
|
||||
|
||||
public WindowsTerminalReader(WindowsDriver driver)
|
||||
: base(STD_HANDLE_TYPE.STD_INPUT_HANDLE)
|
||||
{
|
||||
_encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleCP());
|
||||
_driver = driver;
|
||||
_keyReader = new WindowsKeyReader(Handle);
|
||||
_reader = CreateReader(Handle, _encoding, IsRedirected);
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
return _reader.Read();
|
||||
}
|
||||
|
||||
public string? ReadLine()
|
||||
{
|
||||
return _reader.ReadLine();
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
return _keyReader.ReadKey();
|
||||
}
|
||||
|
||||
private static SynchronizedTextReader CreateReader(SafeHandle handle, Encoding encoding, bool isRedirected)
|
||||
{
|
||||
static Stream Create(SafeHandle handle, bool useFileApis)
|
||||
if (handle.IsInvalid || handle.IsClosed)
|
||||
{
|
||||
if (handle.IsInvalid || handle.IsClosed)
|
||||
{
|
||||
return Stream.Null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WindowsConsoleStream(handle, useFileApis);
|
||||
}
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
var useFileApis = !encoding.IsUnicode() || isRedirected;
|
||||
|
||||
var stream = Create(handle, useFileApis);
|
||||
if (stream == null || stream == Stream.Null)
|
||||
else
|
||||
{
|
||||
return new SynchronizedTextReader(StreamReader.Null);
|
||||
return new WindowsConsoleStream(handle, useFileApis);
|
||||
}
|
||||
|
||||
return new SynchronizedTextReader(
|
||||
new StreamReader(
|
||||
stream: stream,
|
||||
encoding: new EncodingWithoutPreamble(encoding),
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
bufferSize: 4096,
|
||||
leaveOpen: true));
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
var useFileApis = !encoding.IsUnicode() || isRedirected;
|
||||
|
||||
var stream = Create(handle, useFileApis);
|
||||
if (stream == null || stream == Stream.Null)
|
||||
{
|
||||
if (PInvoke.SetConsoleCP((uint)encoding.CodePage))
|
||||
{
|
||||
// TODO 2021-07-31: Recreate text reader
|
||||
_encoding = encoding;
|
||||
}
|
||||
return new SynchronizedTextReader(StreamReader.Null);
|
||||
}
|
||||
|
||||
return new SynchronizedTextReader(
|
||||
new StreamReader(
|
||||
stream: stream,
|
||||
encoding: new EncodingWithoutPreamble(encoding),
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
bufferSize: 4096,
|
||||
leaveOpen: true));
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (PInvoke.SetConsoleCP((uint)encoding.CodePage))
|
||||
{
|
||||
// TODO 2021-07-31: Recreate text reader
|
||||
_encoding = encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +1,67 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Windows.Sdk;
|
||||
namespace Spectre.Terminals.Drivers;
|
||||
|
||||
namespace Spectre.Terminals.Drivers
|
||||
internal sealed class WindowsTerminalWriter : WindowsTerminalHandle, IWindowsTerminalWriter
|
||||
{
|
||||
internal sealed class WindowsTerminalWriter : WindowsTerminalHandle, IWindowsTerminalWriter
|
||||
private readonly string _name;
|
||||
private Encoding _encoding;
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly string _name;
|
||||
private Encoding _encoding;
|
||||
get => _encoding;
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
public Encoding Encoding
|
||||
public WindowsTerminalWriter(STD_HANDLE_TYPE handle)
|
||||
: base(handle)
|
||||
{
|
||||
_name = handle == STD_HANDLE_TYPE.STD_OUTPUT_HANDLE ? "STDOUT" : "STDERR";
|
||||
_encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleOutputCP());
|
||||
}
|
||||
|
||||
public unsafe void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
Write(Handle, buffer);
|
||||
}
|
||||
|
||||
public unsafe void Write(SafeHandle handle, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
get => _encoding;
|
||||
set => SetEncoding(value);
|
||||
return;
|
||||
}
|
||||
|
||||
public WindowsTerminalWriter(STD_HANDLE_TYPE handle)
|
||||
: base(handle)
|
||||
{
|
||||
_name = handle == STD_HANDLE_TYPE.STD_OUTPUT_HANDLE ? "STDOUT" : "STDERR";
|
||||
_encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleOutputCP());
|
||||
}
|
||||
uint written;
|
||||
uint* ptrWritten = &written;
|
||||
|
||||
public unsafe void Write(ReadOnlySpan<byte> buffer)
|
||||
fixed (byte* ptrData = buffer)
|
||||
{
|
||||
Write(Handle, buffer);
|
||||
}
|
||||
|
||||
public unsafe void Write(SafeHandle handle, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
if (PInvoke.WriteFile(handle, ptrData, (uint)buffer.Length, ptrWritten, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint written;
|
||||
uint* ptrWritten = &written;
|
||||
|
||||
fixed (byte* ptrData = buffer)
|
||||
{
|
||||
if (PInvoke.WriteFile(handle, ptrData, (uint)buffer.Length, ptrWritten, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (Marshal.GetLastWin32Error())
|
||||
{
|
||||
case WindowsConstants.ERROR_HANDLE_EOF:
|
||||
case WindowsConstants.ERROR_BROKEN_PIPE:
|
||||
case WindowsConstants.ERROR_NO_DATA:
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Could not write to {_name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
switch (Marshal.GetLastWin32Error())
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
case WindowsConstants.ERROR_HANDLE_EOF:
|
||||
case WindowsConstants.ERROR_BROKEN_PIPE:
|
||||
case WindowsConstants.ERROR_NO_DATA:
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Could not write to {_name}");
|
||||
}
|
||||
}
|
||||
|
||||
if (PInvoke.SetConsoleOutputCP((uint)encoding.CodePage))
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
if (PInvoke.SetConsoleOutputCP((uint)encoding.CodePage))
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal abstract class AnsiInstruction
|
||||
{
|
||||
internal abstract class AnsiInstruction
|
||||
{
|
||||
public abstract void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state);
|
||||
}
|
||||
public abstract void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal static class AnsiInterpreter
|
||||
{
|
||||
internal static class AnsiInterpreter
|
||||
public static void Interpret<TContext>(IAnsiSequenceVisitor<TContext> visitor, TContext context, string text)
|
||||
{
|
||||
public static void Interpret<TContext>(IAnsiSequenceVisitor<TContext> visitor, TContext context, string text)
|
||||
Interpret(visitor, context, text.AsMemory());
|
||||
}
|
||||
|
||||
public static void Interpret<TContext>(IAnsiSequenceVisitor<TContext> visitor, TContext context, ReadOnlyMemory<char> buffer)
|
||||
{
|
||||
if (visitor is null)
|
||||
{
|
||||
Interpret(visitor, context, text.AsMemory());
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
public static void Interpret<TContext>(IAnsiSequenceVisitor<TContext> visitor, TContext context, ReadOnlyMemory<char> buffer)
|
||||
var instructions = AnsiParser.Parse(buffer);
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (visitor is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
var instructions = AnsiParser.Parse(buffer);
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
instruction.Accept(visitor, context);
|
||||
}
|
||||
instruction.Accept(visitor, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,138 +1,137 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a VT/ANSI sequence visitor.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The state.</typeparam>
|
||||
internal interface IAnsiSequenceVisitor<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a VT/ANSI sequence visitor.
|
||||
/// Handles a request to move the cursor backwards.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The state.</typeparam>
|
||||
internal interface IAnsiSequenceVisitor<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor backwards.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorBack(CursorBack instruction, TState state);
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorBack(CursorBack instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor down.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorDown(CursorDown instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor down.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorDown(CursorDown instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor forward.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorForward(CursorForward instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor forward.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorForward(CursorForward instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to set the cursor's horizontal position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to set the cursor's horizontal position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor to the next line
|
||||
/// and to set the cursor column to 0 (zero) position once done.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorNextLine(CursorNextLine instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor to the next line
|
||||
/// and to set the cursor column to 0 (zero) position once done.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorNextLine(CursorNextLine instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to set the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorPosition(CursorPosition instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to set the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorPosition(CursorPosition instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor to the previous line
|
||||
/// and to set the cursor column to 0 (zero) position once done.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorPreviousLine(CursorPreviousLine instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor to the previous line
|
||||
/// and to set the cursor column to 0 (zero) position once done.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorPreviousLine(CursorPreviousLine instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor up.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorUp(CursorUp instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to move the cursor up.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void CursorUp(CursorUp instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to clear the whole, or part of, the display.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EraseInDisplay(EraseInDisplay instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to clear the whole, or part of, the display.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EraseInDisplay(EraseInDisplay instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to clear the whole, or part of, the current line.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EraseInLine(EraseInLine instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to clear the whole, or part of, the current line.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EraseInLine(EraseInLine instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to print text.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void PrintText(PrintText instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to print text.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void PrintText(PrintText instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to restore the cursor position to
|
||||
/// it's previously stored value.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void RestoreCursor(RestoreCursor instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to restore the cursor position to
|
||||
/// it's previously stored value.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void RestoreCursor(RestoreCursor instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to store the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void StoreCursor(StoreCursor instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to store the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void StoreCursor(StoreCursor instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to hide the cursor.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void HideCursor(HideCursor instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to hide the cursor.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void HideCursor(HideCursor instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to show the cursor.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void ShowCursor(ShowCursor instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to show the cursor.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void ShowCursor(ShowCursor instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to enable the alternate buffer.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to enable the alternate buffer.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to disable the alternate buffer.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state);
|
||||
/// <summary>
|
||||
/// Handles a request to disable the alternate buffer.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a request to set the appearance of subsequent characters.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void SelectGraphicRendition(SelectGraphicRendition instruction, TState state);
|
||||
}
|
||||
/// <summary>
|
||||
/// Handles a request to set the appearance of subsequent characters.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
void SelectGraphicRendition(SelectGraphicRendition instruction, TState state);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorBack : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorBack : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorBack(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorBack(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorBack(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorBack(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorDown : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorDown : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorDown(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorDown(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorDown(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorDown(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorForward : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorForward : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorForward(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorForward(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorForward(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorForward(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorHorizontalAbsolute : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorHorizontalAbsolute : AnsiInstruction
|
||||
public int Column { get; }
|
||||
|
||||
public CursorHorizontalAbsolute(int count)
|
||||
{
|
||||
public int Column { get; }
|
||||
Column = count;
|
||||
}
|
||||
|
||||
public CursorHorizontalAbsolute(int count)
|
||||
{
|
||||
Column = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorHorizontalAbsolute(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorHorizontalAbsolute(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorNextLine : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorNextLine : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorNextLine(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorNextLine(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorNextLine(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorNextLine(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorPosition : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorPosition : AnsiInstruction
|
||||
public int Column { get; }
|
||||
public int Row { get; }
|
||||
|
||||
public CursorPosition(int column, int row)
|
||||
{
|
||||
public int Column { get; }
|
||||
public int Row { get; }
|
||||
Column = column;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
public CursorPosition(int column, int row)
|
||||
{
|
||||
Column = column;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorPosition(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorPosition(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorPreviousLine : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorPreviousLine : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorPreviousLine(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorPreviousLine(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorPreviousLine(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorPreviousLine(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class CursorUp : AnsiInstruction
|
||||
{
|
||||
internal sealed class CursorUp : AnsiInstruction
|
||||
public int Count { get; }
|
||||
|
||||
public CursorUp(int count)
|
||||
{
|
||||
public int Count { get; }
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public CursorUp(int count)
|
||||
{
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorUp(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.CursorUp(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class DisableAlternativeBuffer : AnsiInstruction
|
||||
{
|
||||
internal sealed class DisableAlternativeBuffer : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.DisableAlternativeBuffer(this, state);
|
||||
}
|
||||
visitor.DisableAlternativeBuffer(this, state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class EnableAlternativeBuffer : AnsiInstruction
|
||||
{
|
||||
internal sealed class EnableAlternativeBuffer : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.EnableAlternativeBuffer(this, state);
|
||||
}
|
||||
visitor.EnableAlternativeBuffer(this, state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class EraseInDisplay : AnsiInstruction
|
||||
{
|
||||
internal sealed class EraseInDisplay : AnsiInstruction
|
||||
public int Mode { get; }
|
||||
|
||||
public EraseInDisplay(int mode)
|
||||
{
|
||||
public int Mode { get; }
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public EraseInDisplay(int mode)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.EraseInDisplay(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.EraseInDisplay(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class EraseInLine : AnsiInstruction
|
||||
{
|
||||
internal sealed class EraseInLine : AnsiInstruction
|
||||
public int Mode { get; }
|
||||
|
||||
public EraseInLine(int mode)
|
||||
{
|
||||
public int Mode { get; }
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public EraseInLine(int mode)
|
||||
{
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.EraseInLine(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.EraseInLine(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class HideCursor : AnsiInstruction
|
||||
{
|
||||
internal sealed class HideCursor : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.HideCursor(this, state);
|
||||
}
|
||||
visitor.HideCursor(this, state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal sealed class PrintText : AnsiInstruction
|
||||
{
|
||||
internal sealed class PrintText : AnsiInstruction
|
||||
public ReadOnlyMemory<char> Text { get; }
|
||||
|
||||
public PrintText(ReadOnlyMemory<char> text)
|
||||
{
|
||||
public ReadOnlyMemory<char> Text { get; }
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public PrintText(ReadOnlyMemory<char> text)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.PrintText(this, context);
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.PrintText(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class RestoreCursor : AnsiInstruction
|
||||
{
|
||||
internal sealed class RestoreCursor : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.RestoreCursor(this, context);
|
||||
}
|
||||
visitor.RestoreCursor(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,68 +1,65 @@
|
|||
using System.Collections.Generic;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal sealed class SelectGraphicRendition : AnsiInstruction
|
||||
{
|
||||
internal sealed class SelectGraphicRendition : AnsiInstruction
|
||||
public IReadOnlyList<Operation> Operations { get; }
|
||||
|
||||
internal sealed class Operation
|
||||
{
|
||||
public IReadOnlyList<Operation> Operations { get; }
|
||||
|
||||
internal sealed class Operation
|
||||
{
|
||||
public bool Reset { get; set; }
|
||||
public Color? Foreground { get; set; }
|
||||
public Color? Background { get; set; }
|
||||
}
|
||||
|
||||
public SelectGraphicRendition(IEnumerable<Operation> ops)
|
||||
{
|
||||
Operations = ops as IReadOnlyList<Operation>
|
||||
?? new List<Operation>(ops);
|
||||
}
|
||||
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.SelectGraphicRendition(this, state);
|
||||
}
|
||||
public bool Reset { get; set; }
|
||||
public Color? Foreground { get; set; }
|
||||
public Color? Background { get; set; }
|
||||
}
|
||||
|
||||
internal readonly struct Color
|
||||
public SelectGraphicRendition(IEnumerable<Operation> ops)
|
||||
{
|
||||
public int? Number { get; }
|
||||
public int? Red { get; }
|
||||
public int? Green { get; }
|
||||
public int? Blue { get; }
|
||||
Operations = ops as IReadOnlyList<Operation>
|
||||
?? new List<Operation>(ops);
|
||||
}
|
||||
|
||||
public bool IsNumber => Number != null;
|
||||
public bool IsRgb => Red != null && Green != null && Blue != null;
|
||||
|
||||
public Color(int number)
|
||||
{
|
||||
Number = number;
|
||||
Red = null;
|
||||
Green = null;
|
||||
Blue = null;
|
||||
}
|
||||
|
||||
public Color(int red, int green, int blue)
|
||||
{
|
||||
Number = null;
|
||||
Red = red;
|
||||
Green = green;
|
||||
Blue = blue;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Number != null)
|
||||
{
|
||||
return Number.Value.ToString();
|
||||
}
|
||||
else if (Red != null && Green != null && Blue != null)
|
||||
{
|
||||
return $"{Red},{Green},{Blue}";
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.SelectGraphicRendition(this, state);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct Color
|
||||
{
|
||||
public int? Number { get; }
|
||||
public int? Red { get; }
|
||||
public int? Green { get; }
|
||||
public int? Blue { get; }
|
||||
|
||||
public bool IsNumber => Number != null;
|
||||
public bool IsRgb => Red != null && Green != null && Blue != null;
|
||||
|
||||
public Color(int number)
|
||||
{
|
||||
Number = number;
|
||||
Red = null;
|
||||
Green = null;
|
||||
Blue = null;
|
||||
}
|
||||
|
||||
public Color(int red, int green, int blue)
|
||||
{
|
||||
Number = null;
|
||||
Red = red;
|
||||
Green = green;
|
||||
Blue = blue;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Number != null)
|
||||
{
|
||||
return Number.Value.ToString();
|
||||
}
|
||||
else if (Red != null && Green != null && Blue != null)
|
||||
{
|
||||
return $"{Red},{Green},{Blue}";
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class ShowCursor : AnsiInstruction
|
||||
{
|
||||
internal sealed class ShowCursor : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState state)
|
||||
{
|
||||
visitor.ShowCursor(this, state);
|
||||
}
|
||||
visitor.ShowCursor(this, state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal sealed class StoreCursor : AnsiInstruction
|
||||
{
|
||||
internal sealed class StoreCursor : AnsiInstruction
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
public override void Accept<TState>(IAnsiSequenceVisitor<TState> visitor, TState context)
|
||||
{
|
||||
visitor.StoreCursor(this, context);
|
||||
}
|
||||
visitor.StoreCursor(this, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,347 +1,341 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal sealed class AnsiParser
|
||||
{
|
||||
internal sealed class AnsiParser
|
||||
public static IEnumerable<AnsiInstruction> Parse(ReadOnlyMemory<char> buffer)
|
||||
{
|
||||
public static IEnumerable<AnsiInstruction> Parse(ReadOnlyMemory<char> buffer)
|
||||
foreach (var token in Tokenize(buffer))
|
||||
{
|
||||
foreach (var token in Tokenize(buffer))
|
||||
if (token.IsText)
|
||||
{
|
||||
if (token.IsText)
|
||||
yield return new PrintText(token.Text);
|
||||
}
|
||||
else if (token.IsSequence)
|
||||
{
|
||||
var instruction = ParseInstruction(token.Sequence.ToArray());
|
||||
if (instruction != null)
|
||||
{
|
||||
yield return new PrintText(token.Text);
|
||||
}
|
||||
else if (token.IsSequence)
|
||||
{
|
||||
var instruction = ParseInstruction(token.Sequence.ToArray());
|
||||
if (instruction != null)
|
||||
{
|
||||
yield return instruction;
|
||||
}
|
||||
yield return instruction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AnsiToken> Tokenize(ReadOnlyMemory<char> buffer)
|
||||
private static IReadOnlyList<AnsiToken> Tokenize(ReadOnlyMemory<char> buffer)
|
||||
{
|
||||
var result = new List<AnsiToken>();
|
||||
foreach (var (span, isSequence) in AnsiSequenceSplitter.Split(buffer))
|
||||
{
|
||||
var result = new List<AnsiToken>();
|
||||
foreach (var (span, isSequence) in AnsiSequenceSplitter.Split(buffer))
|
||||
if (isSequence)
|
||||
{
|
||||
if (isSequence)
|
||||
var tokens = AnsiSequenceTokenizer.Tokenize(new MemoryCursor(span));
|
||||
if (tokens.Count > 0)
|
||||
{
|
||||
var tokens = AnsiSequenceTokenizer.Tokenize(new MemoryCursor(span));
|
||||
if (tokens.Count > 0)
|
||||
{
|
||||
result.Add(AnsiToken.CreateSequence(tokens));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(AnsiToken.CreateText(span));
|
||||
result.Add(AnsiToken.CreateSequence(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
else
|
||||
{
|
||||
result.Add(AnsiToken.CreateText(span));
|
||||
}
|
||||
}
|
||||
|
||||
private static AnsiInstruction? ParseInstruction(AnsiSequenceToken[] tokens)
|
||||
return result;
|
||||
}
|
||||
|
||||
private static AnsiInstruction? ParseInstruction(AnsiSequenceToken[] tokens)
|
||||
{
|
||||
// Ignore empty sequences or non-CSI sequences (for now)
|
||||
if (tokens.Length == 0 || tokens[0].Type != AnsiSequenceTokenType.Csi)
|
||||
{
|
||||
// Ignore empty sequences or non-CSI sequences (for now)
|
||||
if (tokens.Length == 0 || tokens[0].Type != AnsiSequenceTokenType.Csi)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var terminal = tokens[^1].AsCharacter();
|
||||
if (terminal == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var terminal = tokens[^1].AsCharacter();
|
||||
if (terminal == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the parameters
|
||||
var parameters = new Span<AnsiSequenceToken>(tokens)[1..^1];
|
||||
|
||||
// Query?
|
||||
if (IsQuery(parameters))
|
||||
{
|
||||
parameters = new Span<AnsiSequenceToken>(tokens)[2..^1];
|
||||
return terminal.Value switch
|
||||
{
|
||||
'h' => ParseIntegerInstruction(parameters, value =>
|
||||
{
|
||||
if (value == 25)
|
||||
{
|
||||
return new ShowCursor();
|
||||
}
|
||||
else if (value == 1049)
|
||||
{
|
||||
return new EnableAlternativeBuffer();
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
'l' => ParseIntegerInstruction(parameters, value =>
|
||||
{
|
||||
if (value == 25)
|
||||
{
|
||||
return new HideCursor();
|
||||
}
|
||||
else if (value == 1049)
|
||||
{
|
||||
return new DisableAlternativeBuffer();
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
_ => null, // Unknown query instruction
|
||||
};
|
||||
}
|
||||
// Get the parameters
|
||||
var parameters = new Span<AnsiSequenceToken>(tokens)[1..^1];
|
||||
|
||||
// Query?
|
||||
if (IsQuery(parameters))
|
||||
{
|
||||
parameters = new Span<AnsiSequenceToken>(tokens)[2..^1];
|
||||
return terminal.Value switch
|
||||
{
|
||||
'A' => ParseIntegerInstruction(parameters, count => new CursorUp(count), defaultValue: 1),
|
||||
'B' => ParseIntegerInstruction(parameters, count => new CursorDown(count), defaultValue: 1),
|
||||
'C' => ParseIntegerInstruction(parameters, count => new CursorForward(count), defaultValue: 1),
|
||||
'D' => ParseIntegerInstruction(parameters, count => new CursorBack(count), defaultValue: 1),
|
||||
'E' => ParseIntegerInstruction(parameters, count => new CursorNextLine(count), defaultValue: 1),
|
||||
'F' => ParseIntegerInstruction(parameters, count => new CursorPreviousLine(count), defaultValue: 1),
|
||||
'G' => ParseIntegerInstruction(parameters, count => new CursorHorizontalAbsolute(count), defaultValue: 1),
|
||||
'H' => ParseCursorPosition(parameters),
|
||||
'J' => ParseIntegerInstruction(parameters, count => new EraseInDisplay(count), defaultValue: 0),
|
||||
'K' => ParseIntegerInstruction(parameters, count => new EraseInLine(count), defaultValue: 0),
|
||||
's' => new StoreCursor(),
|
||||
'u' => new RestoreCursor(),
|
||||
'm' => ParseSgr(parameters),
|
||||
_ => null, // Unknown instruction
|
||||
'h' => ParseIntegerInstruction(parameters, value =>
|
||||
{
|
||||
if (value == 25)
|
||||
{
|
||||
return new ShowCursor();
|
||||
}
|
||||
else if (value == 1049)
|
||||
{
|
||||
return new EnableAlternativeBuffer();
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
'l' => ParseIntegerInstruction(parameters, value =>
|
||||
{
|
||||
if (value == 25)
|
||||
{
|
||||
return new HideCursor();
|
||||
}
|
||||
else if (value == 1049)
|
||||
{
|
||||
return new DisableAlternativeBuffer();
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
_ => null, // Unknown query instruction
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsQuery(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
return terminal.Value switch
|
||||
{
|
||||
return tokens.Length > 1 && tokens[0].Type == AnsiSequenceTokenType.Query;
|
||||
'A' => ParseIntegerInstruction(parameters, count => new CursorUp(count), defaultValue: 1),
|
||||
'B' => ParseIntegerInstruction(parameters, count => new CursorDown(count), defaultValue: 1),
|
||||
'C' => ParseIntegerInstruction(parameters, count => new CursorForward(count), defaultValue: 1),
|
||||
'D' => ParseIntegerInstruction(parameters, count => new CursorBack(count), defaultValue: 1),
|
||||
'E' => ParseIntegerInstruction(parameters, count => new CursorNextLine(count), defaultValue: 1),
|
||||
'F' => ParseIntegerInstruction(parameters, count => new CursorPreviousLine(count), defaultValue: 1),
|
||||
'G' => ParseIntegerInstruction(parameters, count => new CursorHorizontalAbsolute(count), defaultValue: 1),
|
||||
'H' => ParseCursorPosition(parameters),
|
||||
'J' => ParseIntegerInstruction(parameters, count => new EraseInDisplay(count), defaultValue: 0),
|
||||
'K' => ParseIntegerInstruction(parameters, count => new EraseInLine(count), defaultValue: 0),
|
||||
's' => new StoreCursor(),
|
||||
'u' => new RestoreCursor(),
|
||||
'm' => ParseSgr(parameters),
|
||||
_ => null, // Unknown instruction
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsQuery(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
{
|
||||
return tokens.Length > 1 && tokens[0].Type == AnsiSequenceTokenType.Query;
|
||||
}
|
||||
|
||||
private static AnsiInstruction? ParseIntegerInstruction(ReadOnlySpan<AnsiSequenceToken> tokens, Func<int, AnsiInstruction?> func, int defaultValue = 1)
|
||||
{
|
||||
if (tokens.Length != 1)
|
||||
{
|
||||
return func(defaultValue);
|
||||
}
|
||||
|
||||
private static AnsiInstruction? ParseIntegerInstruction(ReadOnlySpan<AnsiSequenceToken> tokens, Func<int, AnsiInstruction?> func, int defaultValue = 1)
|
||||
if (tokens[0].Type != AnsiSequenceTokenType.Integer)
|
||||
{
|
||||
if (tokens.Length != 1)
|
||||
{
|
||||
return func(defaultValue);
|
||||
}
|
||||
|
||||
if (tokens[0].Type != AnsiSequenceTokenType.Integer)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return func(ParseInteger(tokens[0].Content.Span));
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int ParseInteger(ReadOnlySpan<char> span, IFormatProvider? provider = null)
|
||||
{
|
||||
return func(ParseInteger(tokens[0].Content.Span));
|
||||
}
|
||||
|
||||
private static int ParseInteger(ReadOnlySpan<char> span, IFormatProvider? provider = null)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
return int.Parse(span, provider: provider);
|
||||
return int.Parse(span, provider: provider);
|
||||
#else
|
||||
return int.Parse(new string(span.ToArray()), provider);
|
||||
return int.Parse(new string(span.ToArray()), provider);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static AnsiInstruction? ParseIntegerInstruction(
|
||||
ReadOnlySpan<AnsiSequenceToken> tokens,
|
||||
Func<int, bool> predicate,
|
||||
Func<AnsiInstruction> func)
|
||||
private static AnsiInstruction? ParseIntegerInstruction(
|
||||
ReadOnlySpan<AnsiSequenceToken> tokens,
|
||||
Func<int, bool> predicate,
|
||||
Func<AnsiInstruction> func)
|
||||
{
|
||||
if (tokens[0].Type != AnsiSequenceTokenType.Integer)
|
||||
{
|
||||
if (tokens[0].Type != AnsiSequenceTokenType.Integer)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (predicate(ParseInteger(tokens[0].Content.Span)))
|
||||
{
|
||||
return func();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CursorPosition? ParseCursorPosition(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
if (predicate(ParseInteger(tokens[0].Content.Span)))
|
||||
{
|
||||
if (tokens.Length == 3)
|
||||
{
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter, AnsiSequenceTokenType.Integer))
|
||||
{
|
||||
// [ROW];[COLUMN]H
|
||||
return new CursorPosition(
|
||||
ParseInteger(tokens[2].Content.Span, CultureInfo.InvariantCulture),
|
||||
ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
else if (tokens.Length == 2)
|
||||
{
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter))
|
||||
{
|
||||
// [ROW];H
|
||||
return new CursorPosition(1, ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter))
|
||||
{
|
||||
// ;[COLUMN]H
|
||||
return new CursorPosition(ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture), 1);
|
||||
}
|
||||
}
|
||||
else if (tokens.Length == 1)
|
||||
{
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer))
|
||||
{
|
||||
// [ROW]H
|
||||
return new CursorPosition(
|
||||
1,
|
||||
ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return func();
|
||||
}
|
||||
|
||||
private static SelectGraphicRendition? ParseSgr(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CursorPosition? ParseCursorPosition(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
{
|
||||
if (tokens.Length == 3)
|
||||
{
|
||||
if (tokens.Length == 0)
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter, AnsiSequenceTokenType.Integer))
|
||||
{
|
||||
// No parameters is treated like a reset
|
||||
return new SelectGraphicRendition(new[]
|
||||
// [ROW];[COLUMN]H
|
||||
return new CursorPosition(
|
||||
ParseInteger(tokens[2].Content.Span, CultureInfo.InvariantCulture),
|
||||
ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
else if (tokens.Length == 2)
|
||||
{
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter))
|
||||
{
|
||||
// [ROW];H
|
||||
return new CursorPosition(1, ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (IsSequence(tokens, AnsiSequenceTokenType.Integer, AnsiSequenceTokenType.Delimiter))
|
||||
{
|
||||
// ;[COLUMN]H
|
||||
return new CursorPosition(ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture), 1);
|
||||
}
|
||||
}
|
||||
else if (tokens.Length == 1)
|
||||
{
|
||||
if (IsSequence(tokens, AnsiSequenceTokenType.Integer))
|
||||
{
|
||||
// [ROW]H
|
||||
return new CursorPosition(
|
||||
1,
|
||||
ParseInteger(tokens[0].Content.Span, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static SelectGraphicRendition? ParseSgr(ReadOnlySpan<AnsiSequenceToken> tokens)
|
||||
{
|
||||
if (tokens.Length == 0)
|
||||
{
|
||||
// No parameters is treated like a reset
|
||||
return new SelectGraphicRendition(new[]
|
||||
{
|
||||
new SelectGraphicRendition.Operation
|
||||
{
|
||||
new SelectGraphicRendition.Operation
|
||||
{
|
||||
Reset = true,
|
||||
},
|
||||
Reset = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
var queue = new Queue<int>();
|
||||
var enumerator = tokens.GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (enumerator.Current.Type == AnsiSequenceTokenType.Integer)
|
||||
{
|
||||
queue.Enqueue(ParseInteger(enumerator.Current.Content.Span));
|
||||
}
|
||||
}
|
||||
|
||||
var ops = new List<SelectGraphicRendition.Operation>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
|
||||
if (current == 0)
|
||||
{
|
||||
// Reset
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Reset = true,
|
||||
});
|
||||
}
|
||||
|
||||
var queue = new Queue<int>();
|
||||
var enumerator = tokens.GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
else if (current >= 30 && current <= 37)
|
||||
{
|
||||
if (enumerator.Current.Type == AnsiSequenceTokenType.Integer)
|
||||
// 3-bit Foreground number
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
queue.Enqueue(ParseInteger(enumerator.Current.Content.Span));
|
||||
}
|
||||
Foreground = new Color(current - 30),
|
||||
});
|
||||
}
|
||||
|
||||
var ops = new List<SelectGraphicRendition.Operation>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
else if (current >= 40 && current <= 47)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
// 3-bit background number
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Background = new Color(current - 40),
|
||||
});
|
||||
}
|
||||
else if (current == 38 || current == 48)
|
||||
{
|
||||
// 3, 4, or 8-bit colors require at least two arguments
|
||||
if (queue.Count < 2)
|
||||
{
|
||||
// Invalid
|
||||
return null;
|
||||
}
|
||||
|
||||
if (current == 0)
|
||||
var isForeground = current == 38;
|
||||
|
||||
current = queue.Dequeue();
|
||||
if (current == 5)
|
||||
{
|
||||
// Reset
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Reset = true,
|
||||
});
|
||||
}
|
||||
else if (current >= 30 && current <= 37)
|
||||
{
|
||||
// 3-bit Foreground number
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Foreground = new Color(current - 30),
|
||||
});
|
||||
}
|
||||
else if (current >= 40 && current <= 47)
|
||||
{
|
||||
// 3-bit background number
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Background = new Color(current - 40),
|
||||
});
|
||||
}
|
||||
else if (current == 38 || current == 48)
|
||||
{
|
||||
// 3, 4, or 8-bit colors require at least two arguments
|
||||
if (queue.Count < 2)
|
||||
// Color number
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
// Invalid
|
||||
return null;
|
||||
}
|
||||
|
||||
var isForeground = current == 38;
|
||||
|
||||
current = queue.Dequeue();
|
||||
if (current == 5)
|
||||
if (isForeground)
|
||||
{
|
||||
// Color number
|
||||
if (queue.Count == 0)
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
// Invalid
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isForeground)
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Foreground = new Color(queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Background = new Color(queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
Foreground = new Color(queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
else if (current == 2)
|
||||
else
|
||||
{
|
||||
// 24-bit colors requires at least three arguments
|
||||
if (queue.Count < 3)
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
// Invalid
|
||||
return null;
|
||||
}
|
||||
Background = new Color(queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (current == 2)
|
||||
{
|
||||
// 24-bit colors requires at least three arguments
|
||||
if (queue.Count < 3)
|
||||
{
|
||||
// Invalid
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isForeground)
|
||||
if (isForeground)
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Foreground = new Color(queue.Dequeue(), queue.Dequeue(), queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
else
|
||||
Foreground = new Color(queue.Dequeue(), queue.Dequeue(), queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
ops.Add(new SelectGraphicRendition.Operation
|
||||
{
|
||||
Background = new Color(queue.Dequeue(), queue.Dequeue(), queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
Background = new Color(queue.Dequeue(), queue.Dequeue(), queue.Dequeue()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SelectGraphicRendition(ops);
|
||||
}
|
||||
|
||||
private static bool IsSequence(ReadOnlySpan<AnsiSequenceToken> tokens, params AnsiSequenceTokenType[] expected)
|
||||
return new SelectGraphicRendition(ops);
|
||||
}
|
||||
|
||||
private static bool IsSequence(ReadOnlySpan<AnsiSequenceToken> tokens, params AnsiSequenceTokenType[] expected)
|
||||
{
|
||||
if (tokens.Length != expected.Length)
|
||||
{
|
||||
if (tokens.Length != expected.Length)
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < tokens.Length; index++)
|
||||
{
|
||||
if (tokens[index].Type != expected[index])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < tokens.Length; index++)
|
||||
{
|
||||
if (tokens[index].Type != expected[index])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal sealed class AnsiToken
|
||||
{
|
||||
internal sealed class AnsiToken
|
||||
private readonly ReadOnlyMemory<char>? _span;
|
||||
private readonly IReadOnlyList<AnsiSequenceToken>? _tokens;
|
||||
|
||||
public ReadOnlyMemory<char> Text => _span ?? ReadOnlyMemory<char>.Empty;
|
||||
public IReadOnlyList<AnsiSequenceToken> Sequence => _tokens ?? Array.Empty<AnsiSequenceToken>();
|
||||
|
||||
public bool IsText => _span != null;
|
||||
public bool IsSequence => _tokens != null;
|
||||
|
||||
private AnsiToken(ReadOnlyMemory<char>? span, IReadOnlyList<AnsiSequenceToken>? tokens)
|
||||
{
|
||||
private readonly ReadOnlyMemory<char>? _span;
|
||||
private readonly IReadOnlyList<AnsiSequenceToken>? _tokens;
|
||||
_span = span;
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<char> Text => _span ?? ReadOnlyMemory<char>.Empty;
|
||||
public IReadOnlyList<AnsiSequenceToken> Sequence => _tokens ?? Array.Empty<AnsiSequenceToken>();
|
||||
public static AnsiToken CreateText(ReadOnlyMemory<char> span)
|
||||
{
|
||||
return new AnsiToken(span, null);
|
||||
}
|
||||
|
||||
public bool IsText => _span != null;
|
||||
public bool IsSequence => _tokens != null;
|
||||
|
||||
private AnsiToken(ReadOnlyMemory<char>? span, IReadOnlyList<AnsiSequenceToken>? tokens)
|
||||
{
|
||||
_span = span;
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
public static AnsiToken CreateText(ReadOnlyMemory<char> span)
|
||||
{
|
||||
return new AnsiToken(span, null);
|
||||
}
|
||||
|
||||
public static AnsiToken CreateSequence(IReadOnlyList<AnsiSequenceToken>? tokens)
|
||||
{
|
||||
return new AnsiToken(null, tokens);
|
||||
}
|
||||
public static AnsiToken CreateSequence(IReadOnlyList<AnsiSequenceToken>? tokens)
|
||||
{
|
||||
return new AnsiToken(null, tokens);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,105 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal static class AnsiSequenceSplitter
|
||||
{
|
||||
internal static class AnsiSequenceSplitter
|
||||
public static List<(ReadOnlyMemory<char> Text, bool IsSequence)> Split(ReadOnlyMemory<char> buffer)
|
||||
{
|
||||
public static List<(ReadOnlyMemory<char> Text, bool IsSequence)> Split(ReadOnlyMemory<char> buffer)
|
||||
var index = 0;
|
||||
var end = 0;
|
||||
|
||||
var result = new List<(ReadOnlyMemory<char>, bool)>();
|
||||
while (index < buffer.Length)
|
||||
{
|
||||
var index = 0;
|
||||
var end = 0;
|
||||
|
||||
var result = new List<(ReadOnlyMemory<char>, bool)>();
|
||||
while (index < buffer.Length)
|
||||
// Encounter ESC?
|
||||
if (buffer.Span[index] == 0x1b)
|
||||
{
|
||||
// Encounter ESC?
|
||||
if (buffer.Span[index] == 0x1b)
|
||||
var start = index;
|
||||
index++;
|
||||
|
||||
if (index > buffer.Length - 1)
|
||||
{
|
||||
var start = index;
|
||||
index++;
|
||||
|
||||
if (index > buffer.Length - 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Not CSI?
|
||||
if (buffer.Span[index] != '[')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
if (index >= buffer.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Any number (including none) of "parameter bytes" in the range 0x30–0x3F
|
||||
if (!EatOptionalRange(buffer, 0x30, 0x3f, ref index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Any number of "intermediate bytes" in the range 0x20–0x2F
|
||||
if (!EatOptionalRange(buffer, 0x20, 0x2f, ref index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// A single "final byte" in the range 0x40–0x7E
|
||||
var terminal = buffer.Span[index];
|
||||
if (terminal < 0x40 || terminal > 0x7e)
|
||||
{
|
||||
throw new InvalidOperationException("Malformed ANSI escape code");
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
// Need to flush?
|
||||
if (end < start)
|
||||
{
|
||||
result.Add((buffer[end..start], false));
|
||||
}
|
||||
|
||||
// Add the escape code to the result
|
||||
end = index;
|
||||
result.Add((buffer[start..end], true));
|
||||
break;
|
||||
}
|
||||
|
||||
// Not CSI?
|
||||
if (buffer.Span[index] != '[')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// More to flush?
|
||||
if (end < buffer.Length)
|
||||
{
|
||||
result.Add((buffer[end..buffer.Length], false));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool EatOptionalRange(ReadOnlyMemory<char> buffer, int start, int stop, ref int index)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (index >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
var current1 = buffer.Span[index];
|
||||
if (current1 < start || current1 > stop)
|
||||
// Any number (including none) of "parameter bytes" in the range 0x30–0x3F
|
||||
if (!EatOptionalRange(buffer, 0x30, 0x3f, ref index))
|
||||
{
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Any number of "intermediate bytes" in the range 0x20–0x2F
|
||||
if (!EatOptionalRange(buffer, 0x20, 0x2f, ref index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// A single "final byte" in the range 0x40–0x7E
|
||||
var terminal = buffer.Span[index];
|
||||
if (terminal < 0x40 || terminal > 0x7e)
|
||||
{
|
||||
throw new InvalidOperationException("Malformed ANSI escape code");
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
// Need to flush?
|
||||
if (end < start)
|
||||
{
|
||||
result.Add((buffer[end..start], false));
|
||||
}
|
||||
|
||||
// Add the escape code to the result
|
||||
end = index;
|
||||
result.Add((buffer[start..end], true));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// More to flush?
|
||||
if (end < buffer.Length)
|
||||
{
|
||||
result.Add((buffer[end..buffer.Length], false));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool EatOptionalRange(ReadOnlyMemory<char> buffer, int start, int stop, ref int index)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (index >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var current1 = buffer.Span[index];
|
||||
if (current1 < start || current1 > stop)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal sealed class AnsiSequenceToken
|
||||
{
|
||||
internal sealed class AnsiSequenceToken
|
||||
public AnsiSequenceTokenType Type { get; }
|
||||
public ReadOnlyMemory<char> Content { get; set; }
|
||||
|
||||
public char? AsCharacter()
|
||||
{
|
||||
public AnsiSequenceTokenType Type { get; }
|
||||
public ReadOnlyMemory<char> Content { get; set; }
|
||||
return Content.Span[Content.Length - 1];
|
||||
}
|
||||
|
||||
public char? AsCharacter()
|
||||
{
|
||||
return Content.Span[Content.Length - 1];
|
||||
}
|
||||
|
||||
public AnsiSequenceToken(AnsiSequenceTokenType type, ReadOnlyMemory<char> value)
|
||||
{
|
||||
Type = type;
|
||||
Content = value;
|
||||
}
|
||||
public AnsiSequenceToken(AnsiSequenceTokenType type, ReadOnlyMemory<char> value)
|
||||
{
|
||||
Type = type;
|
||||
Content = value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
namespace Spectre.Terminals.Emulation
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
internal enum AnsiSequenceTokenType
|
||||
{
|
||||
internal enum AnsiSequenceTokenType
|
||||
{
|
||||
Unknown,
|
||||
Csi,
|
||||
Character,
|
||||
Integer,
|
||||
Delimiter,
|
||||
Bang,
|
||||
Query,
|
||||
Equals,
|
||||
Send,
|
||||
}
|
||||
Unknown,
|
||||
Csi,
|
||||
Character,
|
||||
Integer,
|
||||
Delimiter,
|
||||
Bang,
|
||||
Query,
|
||||
Equals,
|
||||
Send,
|
||||
}
|
||||
|
|
|
@ -1,108 +1,103 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Spectre.Terminals.Emulation;
|
||||
|
||||
namespace Spectre.Terminals.Emulation
|
||||
internal static class AnsiSequenceTokenizer
|
||||
{
|
||||
internal static class AnsiSequenceTokenizer
|
||||
public static IReadOnlyList<AnsiSequenceToken> Tokenize(MemoryCursor buffer)
|
||||
{
|
||||
public static IReadOnlyList<AnsiSequenceToken> Tokenize(MemoryCursor buffer)
|
||||
var result = new List<AnsiSequenceToken>();
|
||||
while (buffer.CanRead)
|
||||
{
|
||||
var result = new List<AnsiSequenceToken>();
|
||||
while (buffer.CanRead)
|
||||
if (!ReadSequenceToken(buffer, out var token))
|
||||
{
|
||||
if (!ReadSequenceToken(buffer, out var token))
|
||||
{
|
||||
// Could not parse, so return an empty result
|
||||
return Array.Empty<AnsiSequenceToken>();
|
||||
}
|
||||
|
||||
result.Add(token);
|
||||
// Could not parse, so return an empty result
|
||||
return Array.Empty<AnsiSequenceToken>();
|
||||
}
|
||||
|
||||
return result;
|
||||
result.Add(token);
|
||||
}
|
||||
|
||||
private static bool ReadSequenceToken(MemoryCursor buffer, [NotNullWhen(true)] out AnsiSequenceToken? token)
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ReadSequenceToken(MemoryCursor buffer, [NotNullWhen(true)] out AnsiSequenceToken? token)
|
||||
{
|
||||
var current = buffer.PeekChar();
|
||||
|
||||
// ESC?
|
||||
if (current == 0x1b)
|
||||
{
|
||||
var current = buffer.PeekChar();
|
||||
|
||||
// ESC?
|
||||
if (current == 0x1b)
|
||||
// CSI?
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
if (buffer.CanRead && buffer.PeekChar() == '[')
|
||||
{
|
||||
// CSI?
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
if (buffer.CanRead && buffer.PeekChar() == '[')
|
||||
{
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(AnsiSequenceTokenType.Csi, buffer.Slice(start, buffer.Position));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unknown escape sequence
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
else if (char.IsNumber(current))
|
||||
{
|
||||
// Number
|
||||
var start = buffer.Position;
|
||||
while (buffer.CanRead)
|
||||
{
|
||||
current = buffer.PeekChar();
|
||||
if (!char.IsNumber(current))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.Discard();
|
||||
}
|
||||
|
||||
var end = buffer.Position;
|
||||
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Integer,
|
||||
buffer.Slice(start, end));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (char.IsLetter(current))
|
||||
{
|
||||
// Letter
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Character,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (current == ';')
|
||||
{
|
||||
// Separator
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Delimiter,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (current == '?')
|
||||
{
|
||||
// Query
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Query,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
token = new AnsiSequenceToken(AnsiSequenceTokenType.Csi, buffer.Slice(start, buffer.Position));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unknown escape sequence
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
else if (char.IsNumber(current))
|
||||
{
|
||||
// Number
|
||||
var start = buffer.Position;
|
||||
while (buffer.CanRead)
|
||||
{
|
||||
current = buffer.PeekChar();
|
||||
if (!char.IsNumber(current))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.Discard();
|
||||
}
|
||||
|
||||
var end = buffer.Position;
|
||||
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Integer,
|
||||
buffer.Slice(start, end));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (char.IsLetter(current))
|
||||
{
|
||||
// Letter
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Character,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (current == ';')
|
||||
{
|
||||
// Separator
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Delimiter,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (current == '?')
|
||||
{
|
||||
// Query
|
||||
var start = buffer.Position;
|
||||
buffer.Discard();
|
||||
token = new AnsiSequenceToken(
|
||||
AnsiSequenceTokenType.Query,
|
||||
buffer.Slice(start, start + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal static class EncodingExtensions
|
||||
{
|
||||
internal static class EncodingExtensions
|
||||
{
|
||||
private const int UnicodeCodePage = 1200;
|
||||
private const int UnicodeCodePage = 1200;
|
||||
|
||||
public static bool IsUnicode(this Encoding encoding)
|
||||
{
|
||||
return encoding.CodePage == UnicodeCodePage;
|
||||
}
|
||||
public static bool IsUnicode(this Encoding encoding)
|
||||
{
|
||||
return encoding.CodePage == UnicodeCodePage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal static class EnumerableExtensions
|
||||
{
|
||||
internal static class EnumerableExtensions
|
||||
public static IEnumerable<(bool First, bool Last, T Item)> Enumerate<T>(this IEnumerator<T> source)
|
||||
{
|
||||
public static IEnumerable<(bool First, bool Last, T Item)> Enumerate<T>(this IEnumerator<T> source)
|
||||
if (source is null)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var first = true;
|
||||
var last = !source.MoveNext();
|
||||
T current;
|
||||
var first = true;
|
||||
var last = !source.MoveNext();
|
||||
T current;
|
||||
|
||||
for (var index = 0; !last; index++)
|
||||
{
|
||||
current = source.Current;
|
||||
last = !source.MoveNext();
|
||||
yield return (first, last, current);
|
||||
first = false;
|
||||
}
|
||||
for (var index = 0; !last; index++)
|
||||
{
|
||||
current = source.Current;
|
||||
last = !source.MoveNext();
|
||||
yield return (first, last, current);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,162 +1,159 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ITerminal"/>.
|
||||
/// </summary>
|
||||
public static partial class ITerminalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ITerminal"/>.
|
||||
/// Moves the cursor.
|
||||
/// </summary>
|
||||
public static partial class ITerminalExtensions
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="direction">The direction to move the cursor.</param>
|
||||
/// <param name="count">The number of steps to move the cursor.</param>
|
||||
public static void MoveCursor(this ITerminal terminal, CursorDirection direction, int count = 1)
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves the cursor.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="direction">The direction to move the cursor.</param>
|
||||
/// <param name="count">The number of steps to move the cursor.</param>
|
||||
public static void MoveCursor(this ITerminal terminal, CursorDirection direction, int count = 1)
|
||||
if (count <= 0)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case CursorDirection.Up:
|
||||
terminal.Write($"\u001b[{count}A");
|
||||
break;
|
||||
case CursorDirection.Down:
|
||||
terminal.Write($"\u001b[{count}B");
|
||||
break;
|
||||
case CursorDirection.Forward:
|
||||
terminal.Write($"\u001b[{count}C");
|
||||
break;
|
||||
case CursorDirection.Back:
|
||||
terminal.Write($"\u001b[{count}D");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="column">The column.</param>
|
||||
public static void SetCursorProsition(this ITerminal terminal, int row, int column)
|
||||
switch (direction)
|
||||
{
|
||||
row = Math.Max(0, row);
|
||||
column = Math.Max(0, column);
|
||||
|
||||
terminal.Write($"\u001b[{row};{column}H");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor down and resets the column position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="count">The number of lines to move down.</param>
|
||||
public static void MoveCursorToNextLine(this ITerminal terminal, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{count}E");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor up and resets the column position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="count">The number of lines to move up.</param>
|
||||
public static void MoveCursorToPreviousLine(this ITerminal terminal, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{count}F");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor to the specified column.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="column">The column to move to.</param>
|
||||
public static void MoveCursorToColumn(this ITerminal terminal, int column)
|
||||
{
|
||||
if (column <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{column}G");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the display.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="option">The clear options.</param>
|
||||
public static void Clear(this ITerminal terminal, ClearDisplay option = ClearDisplay.Everything)
|
||||
{
|
||||
switch (option)
|
||||
{
|
||||
case ClearDisplay.AfterCursor:
|
||||
terminal.Write("\u001b[0J");
|
||||
break;
|
||||
case ClearDisplay.BeforeCursor:
|
||||
terminal.Write("\u001b[1J");
|
||||
break;
|
||||
case ClearDisplay.Everything:
|
||||
terminal.Write("\u001b[2J");
|
||||
break;
|
||||
case ClearDisplay.EverythingAndScrollbackBuffer:
|
||||
terminal.Write("\u001b[3J");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current line.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="option">The clear options.</param>
|
||||
public static void Clear(this ITerminal terminal, ClearLine option)
|
||||
{
|
||||
switch (option)
|
||||
{
|
||||
case ClearLine.AfterCursor:
|
||||
terminal.Write("\u001b[0K");
|
||||
break;
|
||||
case ClearLine.BeforeCursor:
|
||||
terminal.Write("\u001b[1K");
|
||||
break;
|
||||
case ClearLine.WholeLine:
|
||||
terminal.Write("\u001b[2K");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void SaveCursorPosition(this ITerminal terminal)
|
||||
{
|
||||
terminal.Write("\u001b[s");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void RestoreCursorPosition(this ITerminal terminal)
|
||||
{
|
||||
terminal.Write("\u001b[u");
|
||||
case CursorDirection.Up:
|
||||
terminal.Write($"\u001b[{count}A");
|
||||
break;
|
||||
case CursorDirection.Down:
|
||||
terminal.Write($"\u001b[{count}B");
|
||||
break;
|
||||
case CursorDirection.Forward:
|
||||
terminal.Write($"\u001b[{count}C");
|
||||
break;
|
||||
case CursorDirection.Back:
|
||||
terminal.Write($"\u001b[{count}D");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="column">The column.</param>
|
||||
public static void SetCursorProsition(this ITerminal terminal, int row, int column)
|
||||
{
|
||||
row = Math.Max(0, row);
|
||||
column = Math.Max(0, column);
|
||||
|
||||
terminal.Write($"\u001b[{row};{column}H");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor down and resets the column position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="count">The number of lines to move down.</param>
|
||||
public static void MoveCursorToNextLine(this ITerminal terminal, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{count}E");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor up and resets the column position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="count">The number of lines to move up.</param>
|
||||
public static void MoveCursorToPreviousLine(this ITerminal terminal, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{count}F");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the cursor to the specified column.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="column">The column to move to.</param>
|
||||
public static void MoveCursorToColumn(this ITerminal terminal, int column)
|
||||
{
|
||||
if (column <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.Write($"\u001b[{column}G");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the display.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="option">The clear options.</param>
|
||||
public static void Clear(this ITerminal terminal, ClearDisplay option = ClearDisplay.Everything)
|
||||
{
|
||||
switch (option)
|
||||
{
|
||||
case ClearDisplay.AfterCursor:
|
||||
terminal.Write("\u001b[0J");
|
||||
break;
|
||||
case ClearDisplay.BeforeCursor:
|
||||
terminal.Write("\u001b[1J");
|
||||
break;
|
||||
case ClearDisplay.Everything:
|
||||
terminal.Write("\u001b[2J");
|
||||
break;
|
||||
case ClearDisplay.EverythingAndScrollbackBuffer:
|
||||
terminal.Write("\u001b[3J");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current line.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="option">The clear options.</param>
|
||||
public static void Clear(this ITerminal terminal, ClearLine option)
|
||||
{
|
||||
switch (option)
|
||||
{
|
||||
case ClearLine.AfterCursor:
|
||||
terminal.Write("\u001b[0K");
|
||||
break;
|
||||
case ClearLine.BeforeCursor:
|
||||
terminal.Write("\u001b[1K");
|
||||
break;
|
||||
case ClearLine.WholeLine:
|
||||
terminal.Write("\u001b[2K");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void SaveCursorPosition(this ITerminal terminal)
|
||||
{
|
||||
terminal.Write("\u001b[s");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void RestoreCursorPosition(this ITerminal terminal)
|
||||
{
|
||||
terminal.Write("\u001b[u");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,46 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ITerminal"/>.
|
||||
/// </summary>
|
||||
public static partial class ITerminalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ITerminal"/>.
|
||||
/// Writes the specified buffer to the terminal's output handle.
|
||||
/// </summary>
|
||||
public static partial class ITerminalExtensions
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this ITerminal terminal, ReadOnlySpan<char> value)
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the specified buffer to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this ITerminal terminal, ReadOnlySpan<char> value)
|
||||
{
|
||||
terminal.Output.Write(value);
|
||||
}
|
||||
terminal.Output.Write(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified text to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this ITerminal terminal, string? value)
|
||||
{
|
||||
terminal.Output.Write(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes the specified text to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this ITerminal terminal, string? value)
|
||||
{
|
||||
terminal.Output.Write(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an empty line to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void WriteLine(this ITerminal terminal)
|
||||
{
|
||||
terminal.Output.WriteLine();
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes an empty line to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
public static void WriteLine(this ITerminal terminal)
|
||||
{
|
||||
terminal.Output.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified text followed by a line break to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void WriteLine(this ITerminal terminal, string? value)
|
||||
{
|
||||
terminal.Output.WriteLine(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes the specified text followed by a line break to the terminal's output handle.
|
||||
/// </summary>
|
||||
/// <param name="terminal">The terminal.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void WriteLine(this ITerminal terminal, string? value)
|
||||
{
|
||||
terminal.Output.WriteLine(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +1,57 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="TerminalOutput"/>.
|
||||
/// </summary>
|
||||
public static class TerminalOutputExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="TerminalOutput"/>.
|
||||
/// Writes the specified text.
|
||||
/// </summary>
|
||||
public static class TerminalOutputExtensions
|
||||
/// <param name="writer">The writer.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this TerminalOutput writer, string? value)
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the specified text.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void Write(this TerminalOutput writer, string? value)
|
||||
if (writer is null)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.Write(value.AsSpan());
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an empty line.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
public static void WriteLine(this TerminalOutput writer)
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
Write(writer, Environment.NewLine);
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified text followed by a line break.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void WriteLine(this TerminalOutput writer, string? value)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
writer.Write(value.AsSpan());
|
||||
}
|
||||
|
||||
Write(writer, value);
|
||||
Write(writer, Environment.NewLine);
|
||||
/// <summary>
|
||||
/// Writes an empty line.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
public static void WriteLine(this TerminalOutput writer)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
Write(writer, Environment.NewLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified text followed by a line break.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public static void WriteLine(this TerminalOutput writer, string? value)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
Write(writer, value);
|
||||
Write(writer, Environment.NewLine);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,61 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a terminal.
|
||||
/// </summary>
|
||||
public interface ITerminal : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a terminal.
|
||||
/// Gets the name of the terminal driver.
|
||||
/// </summary>
|
||||
public interface ITerminal : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the terminal driver.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the terminal is in raw mode.
|
||||
/// </summary>
|
||||
bool IsRawMode { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the terminal is in raw mode.
|
||||
/// </summary>
|
||||
bool IsRawMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a signal is received.
|
||||
/// </summary>
|
||||
event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
/// <summary>
|
||||
/// Occurs when a signal is received.
|
||||
/// </summary>
|
||||
event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terminal size.
|
||||
/// </summary>
|
||||
TerminalSize? Size { get; }
|
||||
/// <summary>
|
||||
/// Gets the terminal size.
|
||||
/// </summary>
|
||||
TerminalSize? Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDIN</c>.
|
||||
/// </summary>
|
||||
TerminalInput Input { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDIN</c>.
|
||||
/// </summary>
|
||||
TerminalInput Input { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDOUT</c>.
|
||||
/// </summary>
|
||||
TerminalOutput Output { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDOUT</c>.
|
||||
/// </summary>
|
||||
TerminalOutput Output { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDERR</c>.
|
||||
/// </summary>
|
||||
TerminalOutput Error { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TerminalOutput"/> for <c>STDERR</c>.
|
||||
/// </summary>
|
||||
TerminalOutput Error { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Emits a signal.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal to emit.</param>
|
||||
/// <returns><c>true</c> if successful, otherwise false.</returns>
|
||||
bool EmitSignal(TerminalSignal signal);
|
||||
/// <summary>
|
||||
/// Emits a signal.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal to emit.</param>
|
||||
/// <returns><c>true</c> if successful, otherwise false.</returns>
|
||||
bool EmitSignal(TerminalSignal signal);
|
||||
|
||||
/// <summary>
|
||||
/// Enables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool EnableRawMode();
|
||||
/// <summary>
|
||||
/// Enables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool EnableRawMode();
|
||||
|
||||
/// <summary>
|
||||
/// Disables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool DisableRawMode();
|
||||
}
|
||||
/// <summary>
|
||||
/// Disables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool DisableRawMode();
|
||||
}
|
||||
|
|
|
@ -1,64 +1,61 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represent a terminal driver.
|
||||
/// </summary>
|
||||
public interface ITerminalDriver : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a terminal driver.
|
||||
/// Gets the name of the terminal driver.
|
||||
/// </summary>
|
||||
public interface ITerminalDriver : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the terminal driver.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the terminal is in raw mode.
|
||||
/// </summary>
|
||||
bool IsRawMode { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the terminal is in raw mode.
|
||||
/// </summary>
|
||||
bool IsRawMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a signal is received.
|
||||
/// </summary>
|
||||
event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
/// <summary>
|
||||
/// Occurs when a signal is received.
|
||||
/// </summary>
|
||||
event EventHandler<TerminalSignalEventArgs>? Signalled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terminal size.
|
||||
/// </summary>
|
||||
TerminalSize? Size { get; }
|
||||
/// <summary>
|
||||
/// Gets the terminal size.
|
||||
/// </summary>
|
||||
TerminalSize? Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDIN</c>.
|
||||
/// </summary>
|
||||
ITerminalReader Input { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDIN</c>.
|
||||
/// </summary>
|
||||
ITerminalReader Input { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDOUT</c>.
|
||||
/// </summary>
|
||||
ITerminalWriter Output { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDOUT</c>.
|
||||
/// </summary>
|
||||
ITerminalWriter Output { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDERR</c>.
|
||||
/// </summary>
|
||||
ITerminalWriter Error { get; }
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ITerminalWriter"/> for <c>STDERR</c>.
|
||||
/// </summary>
|
||||
ITerminalWriter Error { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Emits a signal.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal to emit.</param>
|
||||
/// <returns><c>true</c> if successful, otherwise false.</returns>
|
||||
bool EmitSignal(TerminalSignal signal);
|
||||
/// <summary>
|
||||
/// Emits a signal.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal to emit.</param>
|
||||
/// <returns><c>true</c> if successful, otherwise false.</returns>
|
||||
bool EmitSignal(TerminalSignal signal);
|
||||
|
||||
/// <summary>
|
||||
/// Enables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool EnableRawMode();
|
||||
/// <summary>
|
||||
/// Enables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool EnableRawMode();
|
||||
|
||||
/// <summary>
|
||||
/// Disables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool DisableRawMode();
|
||||
}
|
||||
/// <summary>
|
||||
/// Disables raw mode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the operation succeeded, otherwise <c>false</c>.</returns>
|
||||
bool DisableRawMode();
|
||||
}
|
||||
|
|
|
@ -1,56 +1,52 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a reader.
|
||||
/// </summary>
|
||||
public interface ITerminalReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a reader.
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public interface ITerminalReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
Encoding Encoding { get; set; }
|
||||
Encoding Encoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a key press is available in the input stream.
|
||||
/// </summary>
|
||||
bool IsKeyAvailable { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a key press is available in the input stream.
|
||||
/// </summary>
|
||||
bool IsKeyAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the reader has been redirected.
|
||||
/// </summary>
|
||||
bool IsRedirected { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the reader has been redirected.
|
||||
/// </summary>
|
||||
bool IsRedirected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next character from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next character from the input stream, or negative one (-1)
|
||||
/// if there are currently no more characters to be read.
|
||||
/// </returns>
|
||||
int Read();
|
||||
/// <summary>
|
||||
/// Reads the next character from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next character from the input stream, or negative one (-1)
|
||||
/// if there are currently no more characters to be read.
|
||||
/// </returns>
|
||||
int Read();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next line of characters from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next line of characters from the input stream, or null if
|
||||
/// no more lines are available.
|
||||
/// </returns>
|
||||
string? ReadLine();
|
||||
/// <summary>
|
||||
/// Reads the next line of characters from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next line of characters from the input stream, or null if
|
||||
/// no more lines are available.
|
||||
/// </returns>
|
||||
string? ReadLine();
|
||||
|
||||
/// <summary>
|
||||
/// Obtains the next character or function key pressed by the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An object that describes the System.ConsoleKey constant and Unicode character,
|
||||
/// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
|
||||
/// object also describes, in a bitwise combination of System.ConsoleModifiers values,
|
||||
/// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
|
||||
/// with the console key.
|
||||
/// </returns>
|
||||
ConsoleKeyInfo ReadKey();
|
||||
}
|
||||
/// <summary>
|
||||
/// Obtains the next character or function key pressed by the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An object that describes the System.ConsoleKey constant and Unicode character,
|
||||
/// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
|
||||
/// object also describes, in a bitwise combination of System.ConsoleModifiers values,
|
||||
/// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
|
||||
/// with the console key.
|
||||
/// </returns>
|
||||
ConsoleKeyInfo ReadKey();
|
||||
}
|
||||
|
|
|
@ -1,27 +1,23 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a writer.
|
||||
/// </summary>
|
||||
public interface ITerminalWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a writer.
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public interface ITerminalWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
Encoding Encoding { get; set; }
|
||||
Encoding Encoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the writer has been redirected.
|
||||
/// </summary>
|
||||
bool IsRedirected { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the writer has been redirected.
|
||||
/// </summary>
|
||||
bool IsRedirected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes a sequence of bytes to the current writer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">A region of memory. This method copies the contents of this region to the writer.</param>
|
||||
void Write(ReadOnlySpan<byte> buffer);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes a sequence of bytes to the current writer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">A region of memory. This method copies the contents of this region to the writer.</param>
|
||||
void Write(ReadOnlySpan<byte> buffer);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text
|
||||
#pragma warning disable SA1623 // Property summary documentation should match accessors
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class AllowNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class DisallowNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class MaybeNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class MaybeNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter may be null.
|
||||
/// </param>
|
||||
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class NotNullIfNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the associated parameter name.</summary>
|
||||
/// <param name="parameterName">
|
||||
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
|
||||
/// </param>
|
||||
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
|
||||
|
||||
/// <summary>Gets the associated parameter name.</summary>
|
||||
public string ParameterName { get; }
|
||||
}
|
||||
|
||||
/// <summary>Applied to a method that will never return under any circumstance.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class DoesNotReturnAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class DoesNotReturnIfAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified parameter value.</summary>
|
||||
/// <param name="parameterValue">
|
||||
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
|
||||
/// the associated parameter matches this value.
|
||||
/// </param>
|
||||
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
|
||||
|
||||
/// <summary>Gets the condition parameter value.</summary>
|
||||
public bool ParameterValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class MemberNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with a field or property member.</summary>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(string member) => Members = new[] { member };
|
||||
|
||||
/// <summary>Initializes the attribute with the list of field and property members.</summary>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(params string[] members) => Members = members;
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
#if INTERNAL_NULLABLE_ATTRIBUTES
|
||||
internal
|
||||
#else
|
||||
public
|
||||
#endif
|
||||
sealed class MemberNotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, string member)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = new[] { member };
|
||||
}
|
||||
|
||||
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = members;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
}
|
||||
#pragma warning restore SA1623 // Property summary documentation should match accessors
|
||||
#pragma warning restore SA1642 // Constructor summary documentation should begin with standard text
|
||||
#endif
|
|
@ -0,0 +1,20 @@
|
|||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Diagnostics;
|
||||
global using System.Diagnostics.CodeAnalysis;
|
||||
global using System.Globalization;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Runtime.InteropServices;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using Microsoft.Windows.Sdk;
|
||||
global using Mono.Unix;
|
||||
global using Mono.Unix.Native;
|
||||
global using Spectre.Terminals.Drivers;
|
||||
global using Spectre.Terminals.Emulation;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
global using System.Buffers;
|
||||
#endif
|
|
@ -1,10 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
|
||||
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
|
||||
<IsPackable>true</IsPackable>
|
||||
<LangVersion>9</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);NU5104;CS1685</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -24,18 +22,12 @@
|
|||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all" />
|
||||
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]" />
|
||||
<PackageReference Include="Nullable" Version="1.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- Nullability support -->
|
||||
<PropertyGroup Condition="$(TargetFramework) == 'netstandard2.0'">
|
||||
<DefineConstants>$(DefineConstants)INTERNAL_NULLABLE_ATTRIBUTES</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
|
||||
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.0.0]" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion>
|
||||
<GenerateNullableAttributes>False</GenerateNullableAttributes>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,117 +1,114 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a terminal.
|
||||
/// </summary>
|
||||
public sealed class Terminal : ITerminal
|
||||
{
|
||||
private static readonly Lazy<ITerminal> _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a terminal.
|
||||
/// Gets a lazily constructed, shared <see cref="ITerminal"/> instance.
|
||||
/// </summary>
|
||||
public sealed class Terminal : ITerminal
|
||||
public static ITerminal Shared => _instance.Value;
|
||||
|
||||
private readonly ITerminalDriver _driver;
|
||||
private readonly object _lock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => _driver.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRawMode { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
{
|
||||
private static readonly Lazy<ITerminal> _instance;
|
||||
add => _driver.Signalled += value;
|
||||
remove => _driver.Signalled += value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a lazily constructed, shared <see cref="ITerminal"/> instance.
|
||||
/// </summary>
|
||||
public static ITerminal Shared => _instance.Value;
|
||||
/// <inheritdoc/>
|
||||
public TerminalSize? Size => _driver.Size;
|
||||
|
||||
private readonly ITerminalDriver _driver;
|
||||
private readonly object _lock;
|
||||
/// <inheritdoc/>
|
||||
public TerminalInput Input { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => _driver.Name;
|
||||
/// <inheritdoc/>
|
||||
public TerminalOutput Output { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRawMode { get; private set; }
|
||||
/// <inheritdoc/>
|
||||
public TerminalOutput Error { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<TerminalSignalEventArgs>? Signalled
|
||||
static Terminal()
|
||||
{
|
||||
_instance = new Lazy<ITerminal>(() => TerminalFactory.Create());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Terminal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="driver">The terminal driver.</param>
|
||||
public Terminal(ITerminalDriver driver)
|
||||
{
|
||||
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
|
||||
_lock = new object();
|
||||
|
||||
Input = new TerminalInput(_driver.Input);
|
||||
Output = new TerminalOutput(_driver.Output);
|
||||
Error = new TerminalOutput(_driver.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Terminal"/> class.
|
||||
/// </summary>
|
||||
~Terminal()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
DisableRawMode();
|
||||
_driver.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
return _driver.EmitSignal(signal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EnableRawMode()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
add => _driver.Signalled += value;
|
||||
remove => _driver.Signalled += value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TerminalSize? Size => _driver.Size;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TerminalInput Input { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TerminalOutput Output { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TerminalOutput Error { get; }
|
||||
|
||||
static Terminal()
|
||||
{
|
||||
_instance = new Lazy<ITerminal>(() => TerminalFactory.Create());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Terminal"/> class.
|
||||
/// </summary>
|
||||
/// <param name="driver">The terminal driver.</param>
|
||||
public Terminal(ITerminalDriver driver)
|
||||
{
|
||||
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
|
||||
_lock = new object();
|
||||
|
||||
Input = new TerminalInput(_driver.Input);
|
||||
Output = new TerminalOutput(_driver.Output);
|
||||
Error = new TerminalOutput(_driver.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Terminal"/> class.
|
||||
/// </summary>
|
||||
~Terminal()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
DisableRawMode();
|
||||
_driver.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EmitSignal(TerminalSignal signal)
|
||||
{
|
||||
return _driver.EmitSignal(signal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EnableRawMode()
|
||||
{
|
||||
lock (_lock)
|
||||
if (IsRawMode)
|
||||
{
|
||||
if (IsRawMode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
IsRawMode = _driver.EnableRawMode();
|
||||
return IsRawMode;
|
||||
return true;
|
||||
}
|
||||
|
||||
IsRawMode = _driver.EnableRawMode();
|
||||
return IsRawMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool DisableRawMode()
|
||||
/// <inheritdoc/>
|
||||
public bool DisableRawMode()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock)
|
||||
if (!IsRawMode)
|
||||
{
|
||||
if (!IsRawMode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
IsRawMode = !_driver.DisableRawMode();
|
||||
return !IsRawMode;
|
||||
return true;
|
||||
}
|
||||
|
||||
IsRawMode = !_driver.DisableRawMode();
|
||||
return !IsRawMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Spectre.Terminals.Drivers;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal static class TerminalFactory
|
||||
{
|
||||
internal static class TerminalFactory
|
||||
public static ITerminal Create()
|
||||
{
|
||||
public static ITerminal Create()
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new Terminal(new WindowsDriver());
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return new Terminal(new LinuxDriver());
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return new Terminal(new MacOSDriver());
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
return new Terminal(new WindowsDriver());
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return new Terminal(new LinuxDriver());
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return new Terminal(new MacOSDriver());
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,132 +1,128 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a mechanism to read input from the terminal.
|
||||
/// </summary>
|
||||
public sealed class TerminalInput
|
||||
{
|
||||
private readonly ITerminalReader _reader;
|
||||
private readonly object _lock;
|
||||
private ITerminalReader? _redirected;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mechanism to read input from the terminal.
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public sealed class TerminalInput
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly ITerminalReader _reader;
|
||||
private readonly object _lock;
|
||||
private ITerminalReader? _redirected;
|
||||
get => GetEncoding();
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public Encoding Encoding
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not input has been redirected.
|
||||
/// </summary>
|
||||
public bool IsRedirected => GetIsRedirected();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a key press is available in the input stream.
|
||||
/// </summary>
|
||||
public bool IsKeyAvailable => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalInput"/> class.
|
||||
/// </summary>
|
||||
/// <param name="reader">The terminal reader.</param>
|
||||
public TerminalInput(ITerminalReader reader)
|
||||
{
|
||||
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects input to the specified <see cref="ITerminalReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">
|
||||
/// The reader to redirect to,
|
||||
/// or <c>null</c>, if the current redirected reader should be removed.
|
||||
/// </param>
|
||||
public void Redirect(ITerminalReader? reader)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
get => GetEncoding();
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not input has been redirected.
|
||||
/// </summary>
|
||||
public bool IsRedirected => GetIsRedirected();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a key press is available in the input stream.
|
||||
/// </summary>
|
||||
public bool IsKeyAvailable => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalInput"/> class.
|
||||
/// </summary>
|
||||
/// <param name="reader">The terminal reader.</param>
|
||||
public TerminalInput(ITerminalReader reader)
|
||||
{
|
||||
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects input to the specified <see cref="ITerminalReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">
|
||||
/// The reader to redirect to,
|
||||
/// or <c>null</c>, if the current redirected reader should be removed.
|
||||
/// </param>
|
||||
public void Redirect(ITerminalReader? reader)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_redirected = reader;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next character from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next character from the input stream, or negative one (-1)
|
||||
/// if there are currently no more characters to be read.
|
||||
/// </returns>
|
||||
public int Read()
|
||||
{
|
||||
return _reader.Read();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next line of characters from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next line of characters from the input stream, or null if
|
||||
/// no more lines are available.
|
||||
/// </returns>
|
||||
public string? ReadLine()
|
||||
{
|
||||
return _reader.ReadLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains the next character or function key pressed by the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An object that describes the System.ConsoleKey constant and Unicode character,
|
||||
/// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
|
||||
/// object also describes, in a bitwise combination of System.ConsoleModifiers values,
|
||||
/// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
|
||||
/// with the console key.
|
||||
/// </returns>
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
return _reader.ReadKey();
|
||||
}
|
||||
|
||||
private bool GetIsRedirected()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetReader().IsRedirected;
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding GetEncoding()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetReader().Encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
GetReader().Encoding = encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private ITerminalReader GetReader()
|
||||
{
|
||||
return _redirected ?? _reader;
|
||||
_redirected = reader;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next character from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next character from the input stream, or negative one (-1)
|
||||
/// if there are currently no more characters to be read.
|
||||
/// </returns>
|
||||
public int Read()
|
||||
{
|
||||
return _reader.Read();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next line of characters from the standard input stream.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next line of characters from the input stream, or null if
|
||||
/// no more lines are available.
|
||||
/// </returns>
|
||||
public string? ReadLine()
|
||||
{
|
||||
return _reader.ReadLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains the next character or function key pressed by the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An object that describes the System.ConsoleKey constant and Unicode character,
|
||||
/// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
|
||||
/// object also describes, in a bitwise combination of System.ConsoleModifiers values,
|
||||
/// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
|
||||
/// with the console key.
|
||||
/// </returns>
|
||||
public ConsoleKeyInfo ReadKey()
|
||||
{
|
||||
return _reader.ReadKey();
|
||||
}
|
||||
|
||||
private bool GetIsRedirected()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetReader().IsRedirected;
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding GetEncoding()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetReader().Encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
GetReader().Encoding = encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private ITerminalReader GetReader()
|
||||
{
|
||||
return _redirected ?? _reader;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,122 +1,114 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
using System.Buffers;
|
||||
#endif
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents a mechanism to write to a terminal output handle.
|
||||
/// </summary>
|
||||
public sealed class TerminalOutput
|
||||
{
|
||||
private readonly ITerminalWriter _writer;
|
||||
private readonly object _lock;
|
||||
private ITerminalWriter? _redirected;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mechanism to write to a terminal output handle.
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public sealed class TerminalOutput
|
||||
public Encoding Encoding
|
||||
{
|
||||
private readonly ITerminalWriter _writer;
|
||||
private readonly object _lock;
|
||||
private ITerminalWriter? _redirected;
|
||||
get => GetEncoding();
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
public Encoding Encoding
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not output has been redirected.
|
||||
/// </summary>
|
||||
public bool IsRedirected => GetIsRedirected();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalOutput"/> class.
|
||||
/// </summary>
|
||||
/// <param name="writer">The terminal writer.</param>
|
||||
public TerminalOutput(ITerminalWriter writer)
|
||||
{
|
||||
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects input to the specified <see cref="ITerminalWriter"/>.
|
||||
/// </summary>
|
||||
/// <param name="writer">
|
||||
/// The writer to redirect to,
|
||||
/// or <c>null</c>, if the current redirected writer should be removed.
|
||||
/// </param>
|
||||
public void Redirect(ITerminalWriter? writer)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
get => GetEncoding();
|
||||
set => SetEncoding(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not output has been redirected.
|
||||
/// </summary>
|
||||
public bool IsRedirected => GetIsRedirected();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalOutput"/> class.
|
||||
/// </summary>
|
||||
/// <param name="writer">The terminal writer.</param>
|
||||
public TerminalOutput(ITerminalWriter writer)
|
||||
{
|
||||
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects input to the specified <see cref="ITerminalWriter"/>.
|
||||
/// </summary>
|
||||
/// <param name="writer">
|
||||
/// The writer to redirect to,
|
||||
/// or <c>null</c>, if the current redirected writer should be removed.
|
||||
/// </param>
|
||||
public void Redirect(ITerminalWriter? writer)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_redirected = writer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified buffer.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void Write(ReadOnlySpan<char> value)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
var len = Encoding.GetByteCount(value);
|
||||
var array = ArrayPool<byte>.Shared.Rent(len);
|
||||
|
||||
try
|
||||
{
|
||||
var span = array.AsSpan(0, len);
|
||||
Encoding.GetBytes(value, span);
|
||||
GetWriter().Write(span);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
#else
|
||||
var chars = value.ToArray();
|
||||
var bytes = Encoding.GetBytes(chars);
|
||||
GetWriter().Write(new Span<byte>(bytes));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding GetEncoding()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetWriter().Encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
GetWriter().Encoding = encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetIsRedirected()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetWriter().IsRedirected;
|
||||
}
|
||||
}
|
||||
|
||||
private ITerminalWriter GetWriter()
|
||||
{
|
||||
return _redirected ?? _writer;
|
||||
_redirected = writer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified buffer.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void Write(ReadOnlySpan<char> value)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
var len = Encoding.GetByteCount(value);
|
||||
var array = ArrayPool<byte>.Shared.Rent(len);
|
||||
|
||||
try
|
||||
{
|
||||
var span = array.AsSpan(0, len);
|
||||
Encoding.GetBytes(value, span);
|
||||
GetWriter().Write(span);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
#else
|
||||
var chars = value.ToArray();
|
||||
var bytes = Encoding.GetBytes(chars);
|
||||
GetWriter().Write(new Span<byte>(bytes));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding GetEncoding()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetWriter().Encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEncoding(Encoding encoding)
|
||||
{
|
||||
if (encoding is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
GetWriter().Encoding = encoding;
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetIsRedirected()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return GetWriter().IsRedirected;
|
||||
}
|
||||
}
|
||||
|
||||
private ITerminalWriter GetWriter()
|
||||
{
|
||||
return _redirected ?? _writer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
namespace Spectre.Terminals
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a terminal signal.
|
||||
/// </summary>
|
||||
public enum TerminalSignal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a terminal signal.
|
||||
/// The SIGINT signal is sent to a process by its controlling terminal
|
||||
/// when a user wishes to interrupt the process.
|
||||
/// This is typically initiated by pressing Ctrl+C.
|
||||
/// </summary>
|
||||
public enum TerminalSignal
|
||||
{
|
||||
/// <summary>
|
||||
/// The SIGINT signal is sent to a process by its controlling terminal
|
||||
/// when a user wishes to interrupt the process.
|
||||
/// This is typically initiated by pressing Ctrl+C.
|
||||
/// </summary>
|
||||
SIGINT,
|
||||
SIGINT,
|
||||
|
||||
/// <summary>
|
||||
/// The SIGQUIT signal is sent to a process by its controlling terminal
|
||||
/// when the user requests that the process quit.
|
||||
/// </summary>
|
||||
SIGQUIT,
|
||||
}
|
||||
/// <summary>
|
||||
/// The SIGQUIT signal is sent to a process by its controlling terminal
|
||||
/// when the user requests that the process quit.
|
||||
/// </summary>
|
||||
SIGQUIT,
|
||||
}
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Provides data for the Signalled event.
|
||||
/// </summary>
|
||||
public sealed class TerminalSignalEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides data for the Signalled event.
|
||||
/// Gets the signal.
|
||||
/// </summary>
|
||||
public sealed class TerminalSignalEventArgs : EventArgs
|
||||
public TerminalSignal Signal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not
|
||||
/// the event was handled or not.
|
||||
/// </summary>
|
||||
public bool Cancel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalSignalEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal.</param>
|
||||
public TerminalSignalEventArgs(TerminalSignal signal)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the signal.
|
||||
/// </summary>
|
||||
public TerminalSignal Signal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not
|
||||
/// the event was handled or not.
|
||||
/// </summary>
|
||||
public bool Cancel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalSignalEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="signal">The signal.</param>
|
||||
public TerminalSignalEventArgs(TerminalSignal signal)
|
||||
{
|
||||
Signal = signal;
|
||||
}
|
||||
Signal = signal;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
/// <summary>
|
||||
/// Represents terminal size.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Width}x{Height}")]
|
||||
public readonly struct TerminalSize : IEquatable<TerminalSize>, IEqualityComparer<TerminalSize>
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents terminal size.
|
||||
/// Gets the terminal width in cells.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Width}x{Height}")]
|
||||
public readonly struct TerminalSize : IEquatable<TerminalSize>, IEqualityComparer<TerminalSize>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terminal height in cells.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalSize"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <param name="height">The height.</param>
|
||||
public TerminalSize(int width, int height)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the terminal width in cells.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terminal height in cells.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TerminalSize other)
|
||||
{
|
||||
return Width == other.Width
|
||||
&& Height == other.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerminalSize"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <param name="height">The height.</param>
|
||||
public TerminalSize(int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TerminalSize x, TerminalSize y)
|
||||
{
|
||||
return x.Width == y.Width
|
||||
&& x.Height == y.Height;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TerminalSize other)
|
||||
{
|
||||
return Width == other.Width
|
||||
&& Height == other.Height;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode([DisallowNull] TerminalSize obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Width, obj.Height);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(TerminalSize x, TerminalSize y)
|
||||
{
|
||||
return x.Width == y.Width
|
||||
&& x.Height == y.Height;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode([DisallowNull] TerminalSize obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Width, obj.Height);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Width}x{Height}";
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Width}x{Height}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,93 +1,89 @@
|
|||
// Parts of this code used from: https://github.com/dotnet/runtime
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal static partial class EncodingHelper
|
||||
{
|
||||
internal static partial class EncodingHelper
|
||||
/// <summary>Environment variables that should be checked, in order, for locale.</summary>
|
||||
/// <remarks>
|
||||
/// One of these environment variables should contain a string of a form consistent with
|
||||
/// the X/Open Portability Guide syntax:
|
||||
/// language[territory][.charset][@modifier]
|
||||
/// We're interested in the charset, as it specifies the encoding used
|
||||
/// for the console.
|
||||
/// </remarks>
|
||||
private static readonly string[] _localeEnvVars = { "LC_ALL", "LC_MESSAGES", "LANG" }; // this ordering codifies the lookup rules prescribed by POSIX
|
||||
|
||||
internal static Encoding RemovePreamble(Encoding encoding)
|
||||
{
|
||||
/// <summary>Environment variables that should be checked, in order, for locale.</summary>
|
||||
/// <remarks>
|
||||
/// One of these environment variables should contain a string of a form consistent with
|
||||
/// the X/Open Portability Guide syntax:
|
||||
/// language[territory][.charset][@modifier]
|
||||
/// We're interested in the charset, as it specifies the encoding used
|
||||
/// for the console.
|
||||
/// </remarks>
|
||||
private static readonly string[] _localeEnvVars = { "LC_ALL", "LC_MESSAGES", "LANG" }; // this ordering codifies the lookup rules prescribed by POSIX
|
||||
|
||||
internal static Encoding RemovePreamble(Encoding encoding)
|
||||
if (encoding.GetPreamble().Length == 0)
|
||||
{
|
||||
if (encoding.GetPreamble().Length == 0)
|
||||
{
|
||||
return encoding;
|
||||
}
|
||||
|
||||
return new EncodingWithoutPreamble(encoding);
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/// <summary>Creates an encoding from the current environment.</summary>
|
||||
/// <returns>The encoding, or null if it could not be determined.</returns>
|
||||
internal static Encoding? GetEncodingFromCharset()
|
||||
return new EncodingWithoutPreamble(encoding);
|
||||
}
|
||||
|
||||
/// <summary>Creates an encoding from the current environment.</summary>
|
||||
/// <returns>The encoding, or null if it could not be determined.</returns>
|
||||
internal static Encoding? GetEncodingFromCharset()
|
||||
{
|
||||
var charset = GetCharset();
|
||||
if (charset != null)
|
||||
{
|
||||
var charset = GetCharset();
|
||||
if (charset != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var encoding = Encoding.GetEncoding(charset);
|
||||
if (encoding != null)
|
||||
{
|
||||
return RemovePreamble(encoding);
|
||||
}
|
||||
}
|
||||
catch
|
||||
var encoding = Encoding.GetEncoding(charset);
|
||||
if (encoding != null)
|
||||
{
|
||||
return RemovePreamble(encoding);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the current charset name from the environment.</summary>
|
||||
/// <returns>The charset name if found; otherwise, null.</returns>
|
||||
private static string? GetCharset()
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Gets the current charset name from the environment.</summary>
|
||||
/// <returns>The charset name if found; otherwise, null.</returns>
|
||||
private static string? GetCharset()
|
||||
{
|
||||
// Find the first of the locale environment variables that's set.
|
||||
string? locale = null;
|
||||
foreach (var envVar in _localeEnvVars)
|
||||
{
|
||||
// Find the first of the locale environment variables that's set.
|
||||
string? locale = null;
|
||||
foreach (var envVar in _localeEnvVars)
|
||||
locale = Environment.GetEnvironmentVariable(envVar);
|
||||
if (!string.IsNullOrWhiteSpace(locale))
|
||||
{
|
||||
locale = Environment.GetEnvironmentVariable(envVar);
|
||||
if (!string.IsNullOrWhiteSpace(locale))
|
||||
{
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we found one, try to parse it.
|
||||
// The locale string is expected to be of a form that matches the
|
||||
// X/Open Portability Guide syntax: language[_territory][.charset][@modifier]
|
||||
if (locale != null)
|
||||
{
|
||||
// Does it contain the optional charset?
|
||||
var dotPos = locale.IndexOf('.');
|
||||
if (dotPos >= 0)
|
||||
{
|
||||
dotPos++;
|
||||
var atPos = locale.IndexOf('@', dotPos + 1);
|
||||
|
||||
// return the charset from the locale, stripping off everything else
|
||||
var charset = atPos < dotPos ?
|
||||
locale.Substring(dotPos) : // no modifier
|
||||
locale.Substring(dotPos, atPos - dotPos); // has modifier
|
||||
return charset.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
// no charset found; the default will be used
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we found one, try to parse it.
|
||||
// The locale string is expected to be of a form that matches the
|
||||
// X/Open Portability Guide syntax: language[_territory][.charset][@modifier]
|
||||
if (locale != null)
|
||||
{
|
||||
// Does it contain the optional charset?
|
||||
var dotPos = locale.IndexOf('.');
|
||||
if (dotPos >= 0)
|
||||
{
|
||||
dotPos++;
|
||||
var atPos = locale.IndexOf('@', dotPos + 1);
|
||||
|
||||
// return the charset from the locale, stripping off everything else
|
||||
var charset = atPos < dotPos ?
|
||||
locale.Substring(dotPos) : // no modifier
|
||||
locale.Substring(dotPos, atPos - dotPos); // has modifier
|
||||
return charset.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
// no charset found; the default will be used
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal static partial class EncodingHelper
|
||||
{
|
||||
internal static partial class EncodingHelper
|
||||
static EncodingHelper()
|
||||
{
|
||||
static EncodingHelper()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
}
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
}
|
||||
|
||||
internal static Encoding GetEncodingFromCodePage(int codePage)
|
||||
{
|
||||
return Encoding.GetEncoding(codePage) ?? Encoding.UTF8;
|
||||
}
|
||||
internal static Encoding GetEncodingFromCodePage(int codePage)
|
||||
{
|
||||
return Encoding.GetEncoding(codePage) ?? Encoding.UTF8;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,177 +1,173 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal sealed class EncodingWithoutPreamble : Encoding
|
||||
{
|
||||
internal sealed class EncodingWithoutPreamble : Encoding
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public override string BodyName => _encoding.BodyName;
|
||||
|
||||
public override int CodePage => _encoding.CodePage;
|
||||
|
||||
public override bool IsSingleByte => _encoding.IsSingleByte;
|
||||
|
||||
public override string EncodingName => _encoding.EncodingName;
|
||||
|
||||
public override string WebName
|
||||
{
|
||||
private readonly Encoding _encoding;
|
||||
get { return _encoding.WebName; }
|
||||
}
|
||||
|
||||
public override string BodyName => _encoding.BodyName;
|
||||
public EncodingWithoutPreamble(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
public override int CodePage => _encoding.CodePage;
|
||||
public override byte[] GetPreamble()
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public override bool IsSingleByte => _encoding.IsSingleByte;
|
||||
public override unsafe int GetByteCount(char* chars, int count)
|
||||
{
|
||||
return _encoding.GetByteCount(chars, count);
|
||||
}
|
||||
|
||||
public override string EncodingName => _encoding.EncodingName;
|
||||
public override int GetByteCount(char[] chars)
|
||||
{
|
||||
return _encoding.GetByteCount(chars);
|
||||
}
|
||||
|
||||
public override string WebName
|
||||
{
|
||||
get { return _encoding.WebName; }
|
||||
}
|
||||
public override int GetByteCount(string s)
|
||||
{
|
||||
return _encoding.GetByteCount(s);
|
||||
}
|
||||
|
||||
public EncodingWithoutPreamble(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
public override int GetByteCount(char[] chars, int index, int count)
|
||||
{
|
||||
return _encoding.GetByteCount(chars, index, count);
|
||||
}
|
||||
|
||||
public override byte[] GetPreamble()
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
public override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount)
|
||||
{
|
||||
return _encoding.GetBytes(chars, charCount, bytes, byteCount);
|
||||
}
|
||||
|
||||
public override unsafe int GetByteCount(char* chars, int count)
|
||||
{
|
||||
return _encoding.GetByteCount(chars, count);
|
||||
}
|
||||
public override byte[] GetBytes(char[] chars)
|
||||
{
|
||||
return _encoding.GetBytes(chars);
|
||||
}
|
||||
|
||||
public override int GetByteCount(char[] chars)
|
||||
{
|
||||
return _encoding.GetByteCount(chars);
|
||||
}
|
||||
public override byte[] GetBytes(char[] chars, int index, int count)
|
||||
{
|
||||
return _encoding.GetBytes(chars, index, count);
|
||||
}
|
||||
|
||||
public override int GetByteCount(string s)
|
||||
{
|
||||
return _encoding.GetByteCount(s);
|
||||
}
|
||||
public override byte[] GetBytes(string s)
|
||||
{
|
||||
return _encoding.GetBytes(s);
|
||||
}
|
||||
|
||||
public override int GetByteCount(char[] chars, int index, int count)
|
||||
{
|
||||
return _encoding.GetByteCount(chars, index, count);
|
||||
}
|
||||
public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex)
|
||||
{
|
||||
return _encoding.GetBytes(s, charIndex, charCount, bytes, byteIndex);
|
||||
}
|
||||
|
||||
public override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount)
|
||||
{
|
||||
return _encoding.GetBytes(chars, charCount, bytes, byteCount);
|
||||
}
|
||||
public override unsafe int GetCharCount(byte* bytes, int count)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes, count);
|
||||
}
|
||||
|
||||
public override byte[] GetBytes(char[] chars)
|
||||
{
|
||||
return _encoding.GetBytes(chars);
|
||||
}
|
||||
public override int GetCharCount(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes);
|
||||
}
|
||||
|
||||
public override byte[] GetBytes(char[] chars, int index, int count)
|
||||
{
|
||||
return _encoding.GetBytes(chars, index, count);
|
||||
}
|
||||
public override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount)
|
||||
{
|
||||
return _encoding.GetChars(bytes, byteCount, chars, charCount);
|
||||
}
|
||||
|
||||
public override byte[] GetBytes(string s)
|
||||
{
|
||||
return _encoding.GetBytes(s);
|
||||
}
|
||||
public override char[] GetChars(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetChars(bytes);
|
||||
}
|
||||
|
||||
public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex)
|
||||
{
|
||||
return _encoding.GetBytes(s, charIndex, charCount, bytes, byteIndex);
|
||||
}
|
||||
public override char[] GetChars(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetChars(bytes, index, count);
|
||||
}
|
||||
|
||||
public override unsafe int GetCharCount(byte* bytes, int count)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes, count);
|
||||
}
|
||||
public override Decoder GetDecoder()
|
||||
{
|
||||
return _encoding.GetDecoder();
|
||||
}
|
||||
|
||||
public override int GetCharCount(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes);
|
||||
}
|
||||
public override Encoder GetEncoder()
|
||||
{
|
||||
return _encoding.GetEncoder();
|
||||
}
|
||||
|
||||
public override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount)
|
||||
{
|
||||
return _encoding.GetChars(bytes, byteCount, chars, charCount);
|
||||
}
|
||||
public override string GetString(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetString(bytes);
|
||||
}
|
||||
|
||||
public override char[] GetChars(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetChars(bytes);
|
||||
}
|
||||
public override bool IsAlwaysNormalized(NormalizationForm form)
|
||||
{
|
||||
return _encoding.IsAlwaysNormalized(form);
|
||||
}
|
||||
|
||||
public override char[] GetChars(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetChars(bytes, index, count);
|
||||
}
|
||||
public override string GetString(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetString(bytes, index, count);
|
||||
}
|
||||
|
||||
public override Decoder GetDecoder()
|
||||
{
|
||||
return _encoding.GetDecoder();
|
||||
}
|
||||
public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
|
||||
{
|
||||
return _encoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex);
|
||||
}
|
||||
|
||||
public override Encoder GetEncoder()
|
||||
{
|
||||
return _encoding.GetEncoder();
|
||||
}
|
||||
public override int GetCharCount(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes, index, count);
|
||||
}
|
||||
|
||||
public override string GetString(byte[] bytes)
|
||||
{
|
||||
return _encoding.GetString(bytes);
|
||||
}
|
||||
public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
|
||||
{
|
||||
return _encoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
|
||||
}
|
||||
|
||||
public override bool IsAlwaysNormalized(NormalizationForm form)
|
||||
{
|
||||
return _encoding.IsAlwaysNormalized(form);
|
||||
}
|
||||
public override int GetMaxByteCount(int charCount)
|
||||
{
|
||||
return _encoding.GetMaxByteCount(charCount);
|
||||
}
|
||||
|
||||
public override string GetString(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetString(bytes, index, count);
|
||||
}
|
||||
|
||||
public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
|
||||
{
|
||||
return _encoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex);
|
||||
}
|
||||
|
||||
public override int GetCharCount(byte[] bytes, int index, int count)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes, index, count);
|
||||
}
|
||||
|
||||
public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
|
||||
{
|
||||
return _encoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
|
||||
}
|
||||
|
||||
public override int GetMaxByteCount(int charCount)
|
||||
{
|
||||
return _encoding.GetMaxByteCount(charCount);
|
||||
}
|
||||
|
||||
public override int GetMaxCharCount(int byteCount)
|
||||
{
|
||||
return _encoding.GetMaxByteCount(byteCount);
|
||||
}
|
||||
public override int GetMaxCharCount(int byteCount)
|
||||
{
|
||||
return _encoding.GetMaxByteCount(byteCount);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
public override ReadOnlySpan<byte> Preamble => ReadOnlySpan<byte>.Empty;
|
||||
public override ReadOnlySpan<byte> Preamble => ReadOnlySpan<byte>.Empty;
|
||||
|
||||
public override int GetByteCount(ReadOnlySpan<char> chars)
|
||||
{
|
||||
return _encoding.GetByteCount(chars);
|
||||
}
|
||||
|
||||
public override int GetBytes(ReadOnlySpan<char> chars, Span<byte> bytes)
|
||||
{
|
||||
return _encoding.GetBytes(chars, bytes);
|
||||
}
|
||||
|
||||
public override int GetCharCount(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes);
|
||||
}
|
||||
|
||||
public override int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars)
|
||||
{
|
||||
return _encoding.GetChars(bytes, chars);
|
||||
}
|
||||
#endif
|
||||
public override int GetByteCount(ReadOnlySpan<char> chars)
|
||||
{
|
||||
return _encoding.GetByteCount(chars);
|
||||
}
|
||||
|
||||
public override int GetBytes(ReadOnlySpan<char> chars, Span<byte> bytes)
|
||||
{
|
||||
return _encoding.GetBytes(chars, bytes);
|
||||
}
|
||||
|
||||
public override int GetCharCount(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
return _encoding.GetCharCount(bytes);
|
||||
}
|
||||
|
||||
public override int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars)
|
||||
{
|
||||
return _encoding.GetChars(bytes, chars);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -1,69 +1,66 @@
|
|||
using System;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal sealed class MemoryCursor
|
||||
{
|
||||
internal sealed class MemoryCursor
|
||||
private readonly ReadOnlyMemory<char> _buffer;
|
||||
private int _position;
|
||||
|
||||
public bool CanRead => _position < _buffer.Length;
|
||||
public int Position => _position;
|
||||
|
||||
public MemoryCursor(ReadOnlyMemory<char> buffer)
|
||||
{
|
||||
private readonly ReadOnlyMemory<char> _buffer;
|
||||
private int _position;
|
||||
_buffer = buffer;
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
public bool CanRead => _position < _buffer.Length;
|
||||
public int Position => _position;
|
||||
|
||||
public MemoryCursor(ReadOnlyMemory<char> buffer)
|
||||
public int Peek()
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_position = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int Peek()
|
||||
return _buffer.Span[_position];
|
||||
}
|
||||
|
||||
public char PeekChar()
|
||||
{
|
||||
return (char)Peek();
|
||||
}
|
||||
|
||||
public char ReadChar()
|
||||
{
|
||||
return (char)Read();
|
||||
}
|
||||
|
||||
public void Discard()
|
||||
{
|
||||
Read();
|
||||
}
|
||||
|
||||
public void Discard(char expected)
|
||||
{
|
||||
var read = ReadChar();
|
||||
if (read != expected)
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return _buffer.Span[_position];
|
||||
}
|
||||
|
||||
public char PeekChar()
|
||||
{
|
||||
return (char)Peek();
|
||||
}
|
||||
|
||||
public char ReadChar()
|
||||
{
|
||||
return (char)Read();
|
||||
}
|
||||
|
||||
public void Discard()
|
||||
{
|
||||
Read();
|
||||
}
|
||||
|
||||
public void Discard(char expected)
|
||||
{
|
||||
var read = ReadChar();
|
||||
if (read != expected)
|
||||
{
|
||||
throw new InvalidOperationException($"Expected '{expected}' but got '{read}'.");
|
||||
}
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
var result = Peek();
|
||||
if (result != -1)
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<char> Slice(int start, int stop)
|
||||
{
|
||||
return _buffer.Slice(start, stop - start);
|
||||
throw new InvalidOperationException($"Expected '{expected}' but got '{read}'.");
|
||||
}
|
||||
}
|
||||
|
||||
public int Read()
|
||||
{
|
||||
var result = Peek();
|
||||
if (result != -1)
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<char> Slice(int start, int stop)
|
||||
{
|
||||
return _buffer.Slice(start, stop - start);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,98 +1,93 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
namespace Spectre.Terminals;
|
||||
|
||||
namespace Spectre.Terminals
|
||||
internal sealed class SynchronizedTextReader : TextReader
|
||||
{
|
||||
internal sealed class SynchronizedTextReader : TextReader
|
||||
private readonly TextReader _inner;
|
||||
private readonly object _lock;
|
||||
|
||||
public SynchronizedTextReader(TextReader reader)
|
||||
{
|
||||
private readonly TextReader _inner;
|
||||
private readonly object _lock;
|
||||
_inner = reader ?? throw new ArgumentNullException(nameof(reader));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public SynchronizedTextReader(TextReader reader)
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_inner = reader ?? throw new ArgumentNullException(nameof(reader));
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_inner.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Peek()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Read()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Read(char[] buffer, int index, int count)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return _inner.Read(buffer, index, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override int ReadBlock(char[] buffer, int index, int count)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return _inner.ReadBlock(buffer, index, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override string? ReadLine()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ReadToEnd()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<string?> ReadLineAsync()
|
||||
{
|
||||
return Task.FromResult(ReadLine());
|
||||
}
|
||||
|
||||
public override Task<string> ReadToEndAsync()
|
||||
{
|
||||
return Task.FromResult(ReadToEnd());
|
||||
}
|
||||
|
||||
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return Task.FromResult(ReadBlock(buffer, index, count));
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return Task.FromResult(Read(buffer, index, count));
|
||||
_inner.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Peek()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Read()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
public override int Read(char[] buffer, int index, int count)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return _inner.Read(buffer, index, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override int ReadBlock(char[] buffer, int index, int count)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return _inner.ReadBlock(buffer, index, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override string? ReadLine()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ReadToEnd()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inner.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<string?> ReadLineAsync()
|
||||
{
|
||||
return Task.FromResult(ReadLine());
|
||||
}
|
||||
|
||||
public override Task<string> ReadToEndAsync()
|
||||
{
|
||||
return Task.FromResult(ReadToEnd());
|
||||
}
|
||||
|
||||
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return Task.FromResult(ReadBlock(buffer, index, count));
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(char[] buffer, int index, int count)
|
||||
{
|
||||
// TODO 2021-07-31: Validate input
|
||||
return Task.FromResult(Read(buffer, index, count));
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче