Upgrade solution to C# 10 and .NET 6

This commit is contained in:
Patrik Svensson 2022-06-13 12:36:58 +02:00 коммит произвёл Patrik Svensson
Родитель 5ba38e544d
Коммит 10cef58def
96 изменённых файлов: 4482 добавлений и 4503 удалений

Просмотреть файл

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

2
.github/workflows/ci.yaml поставляемый
Просмотреть файл

@ -39,8 +39,6 @@ jobs:
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.301
- name: Build
shell: bash

4
.github/workflows/publish.yaml поставляемый
Просмотреть файл

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

Просмотреть файл

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