This commit is contained in:
Patrik Svensson 2021-05-26 18:32:03 +02:00
Коммит a23b083ac5
49 изменённых файлов: 1692 добавлений и 0 удалений

12
.config/dotnet-tools.json Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-example": {
"version": "1.3.1",
"commands": [
"dotnet-example"
]
}
}
}

178
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,178 @@
root = true
[*]
charset = utf-8
end_of_line = CRLF
indent_style = space
indent_size = 4
insert_final_newline = false
trim_trailing_whitespace = true
[*.sln]
indent_style = tab
[*.{csproj,vbproj,vcxproj,vcxproj.filters}]
indent_size = 2
[*.{xml,config,props,targets,nuspec,ruleset}]
indent_size = 2
[*.{yml,yaml}]
indent_size = 2
[*.json]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.sh]
end_of_line = lf
[*.cs]
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Suggest more modern language features when available
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
# Non-private static fields are PascalCase
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
# Non-private readonly fields are PascalCase
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
# Constants are PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_style.constant_style.capitalization = pascal_case
# Instance fields are camelCase and start with _
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
dotnet_naming_style.camel_case_style.capitalization = camel_case
# Local functions are PascalCase
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_style.local_function_style.capitalization = pascal_case
# By default, name items with PascalCase
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Blocks are allowed
csharp_prefer_braces = true:silent
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
# warning RS0037: PublicAPI.txt is missing '#nullable enable'
dotnet_diagnostic.RS0037.severity = none

93
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,93 @@
# Misc folders
[Bb]in/
[Oo]bj/
[Tt]emp/
[Pp]ackages/
/.artifacts/
/[Tt]ools/
.idea
.DS_Store
# Cakeup
cakeup-x86_64-latest.exe
# .NET Core CLI
/.dotnet/
/.packages/
dotnet-install.sh*
*.lock.json
# Visual Studio
.vs/
.vscode/
launchSettings.json
*.sln.ide/
# Rider
src/.idea/**/workspace.xml
src/.idea/**/tasks.xml
src/.idea/dictionaries
src/.idea/**/dataSources/
src/.idea/**/dataSources.ids
src/.idea/**/dataSources.xml
src/.idea/**/dataSources.local.xml
src/.idea/**/sqlDataSources.xml
src/.idea/**/dynamic.xml
src/.idea/**/uiDesigner.xml
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
*.userprefs
*.GhostDoc.xml
*StyleCop.Cache
# Build results
[Dd]ebug/
[Rr]elease/
x64/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.log
*.vspscc
*.vssscc
.builds
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# ReSharper is a .NET coding add-in
_ReSharper*
# NCrunch
.*crunch*.local.xml
_NCrunch_*
# NuGet Packages Directory
packages
# Windows
Thumbs.db
*.received.*
node_modules

21
LICENSE.md Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Patrik Svensson, Phil Scott
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
dotnet-tools.json Normal file
Просмотреть файл

@ -0,0 +1,24 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "1.1.0",
"commands": [
"dotnet-cake"
]
},
"gpr": {
"version": "0.1.224",
"commands": [
"gpr"
]
},
"dotnet-example": {
"version": "1.3.1",
"commands": [
"dotnet-example"
]
}
}
}

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

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ExampleTitle>Ansi</ExampleTitle>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Terminal\Terminal.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,18 @@
using System;
using Spectre.Terminal;
namespace DotNet
{
class Program
{
static void Main(string[] args)
{
var terminal = new Terminal(new FallbackTerminal());
terminal.Output.Write("Hello World!");
Console.ReadKey();
terminal.Output.Write("Hello World!");
}
}
}

7
global.json Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"projects": [ "src" ],
"sdk": {
"version": "5.0.202",
"rollForward": "latestPatch"
}
}

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

@ -0,0 +1,6 @@
<SolutionConfiguration>
<Settings>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>

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

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
using Spectre.Terminal.Ansi;
using Xunit;
namespace Spectre.Terminal.Tests
{
public sealed class AnsiSequenceTests
{
public sealed class TheClearMethod
{
[Fact]
public void Should_Clean_ANSI_Escape_Sequences()
{
// Given, When
var result = AnsiSequence.Clear("\u001b[2KHello \u001b[2BWorld!\u001b[1G!");
// Then
result.ShouldBe("Hello World!!");
}
}
public sealed class TheInterpretMethod
{
[Fact]
public void Should_Run_Sequence()
{
// Given
var printer = new AnsiPrinter();
var state = new StringBuilder();
// When
AnsiSequence.Interpret(
printer, state,
"\u001b[2KHello \u001b[2BWorld!\u001b[1G!");
// Then
state.ToString()
.ShouldBe("[EL2]Hello [CUD2]World![CHA1]!");
}
private sealed class AnsiPrinter : AnsiSequenceVisitor<StringBuilder>
{
protected override void CursorDown(CursorDown instruction, StringBuilder context)
{
context.Append($"[CUD{instruction.Count}]");
}
protected override void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, StringBuilder context)
{
context.Append($"[CHA{instruction.Count}]");
}
protected override void EraseInLine(EraseInLine instruction, StringBuilder context)
{
context.Append($"[EL{instruction.Mode}]");
}
protected override void PrintText(PrintText instruction, StringBuilder context)
{
context.Append(instruction.Text.ToString());
}
}
}
}
}

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

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Terminal\Terminal.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spectre.Terminal.Tests
{
public static class ShouldlyExtensions
{
public static T And<T>(this T item)
{
return item;
}
}
}

70
src/Terminal.sln Normal file
Просмотреть файл

@ -0,0 +1,70 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.6.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal", "Terminal\Terminal.csproj", "{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Tests", "Terminal.Tests\Terminal.Tests.csproj", "{B9AA6477-1C5F-44CE-87C1-219948DB8772}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02C78B96-9010-48EC-ADB5-4F48884F8937}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ansi", "..\examples\DotNet\Ansi.csproj", "{23DA1EC0-52D0-4420-828B-2C648E0F63E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x64.ActiveCfg = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x64.Build.0 = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x86.ActiveCfg = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x86.Build.0 = Debug|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|Any CPU.Build.0 = Release|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x64.ActiveCfg = Release|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x64.Build.0 = Release|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x86.ActiveCfg = Release|Any CPU
{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x86.Build.0 = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x64.ActiveCfg = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x64.Build.0 = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x86.ActiveCfg = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x86.Build.0 = Debug|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|Any CPU.Build.0 = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x64.ActiveCfg = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x64.Build.0 = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x86.ActiveCfg = Release|Any CPU
{B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x86.Build.0 = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|x64.ActiveCfg = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|x64.Build.0 = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|x86.ActiveCfg = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Debug|x86.Build.0 = Debug|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|Any CPU.Build.0 = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|x64.ActiveCfg = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|x64.Build.0 = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|x86.ActiveCfg = Release|Any CPU
{23DA1EC0-52D0-4420-828B-2C648E0F63E7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{23DA1EC0-52D0-4420-828B-2C648E0F63E7} = {02C78B96-9010-48EC-ADB5-4F48884F8937}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3CAB9C8-0317-476E-AEA8-66EF76BA8661}
EndGlobalSection
EndGlobal

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

@ -0,0 +1,7 @@
namespace Spectre.Terminal.Ansi
{
public abstract class AnsiInstruction
{
public abstract void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context);
}
}

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

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Spectre.Terminal.Ansi
{
public static class AnsiSequence
{
public static string Clear(string text)
{
return AnsiSequenceCleaner.Instance.Run(text);
}
public static void Interpret<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context, string text)
{
if (visitor is null)
{
throw new ArgumentNullException(nameof(visitor));
}
var instructions = AnsiInstructionParser.Parse(text.AsMemory());
foreach (var instruction in instructions)
{
instruction.Accept(visitor, context);
}
}
}
}

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

@ -0,0 +1,21 @@
using System.Text;
namespace Spectre.Terminal.Ansi
{
internal sealed class AnsiSequenceCleaner : AnsiSequenceVisitor<StringBuilder>
{
internal static AnsiSequenceCleaner Instance { get; } = new AnsiSequenceCleaner();
public string Run(string text)
{
var context = new StringBuilder();
AnsiSequence.Interpret(Instance, context, text);
return context.ToString();
}
protected internal override void PrintText(PrintText instruction, StringBuilder context)
{
context.Append(instruction.Text);
}
}
}

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

@ -0,0 +1,59 @@
using System;
namespace Spectre.Terminal.Ansi
{
public abstract class AnsiSequenceVisitor<TContext>
{
protected internal virtual void CursorBack(CursorBack instruction, TContext context)
{
}
protected internal virtual void CursorDown(CursorDown instruction, TContext context)
{
}
protected internal virtual void CursorForward(CursorForward instruction, TContext context)
{
}
protected internal virtual void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TContext context)
{
}
protected internal virtual void CursorNextLine(CursorNextLine instruction, TContext context)
{
}
protected internal virtual void CursorPosition(CursorPosition instruction, TContext context)
{
}
protected internal virtual void CursorPreviousLine(CursorPreviousLine instruction, TContext context)
{
}
protected internal virtual void CursorUp(CursorUp instruction, TContext context)
{
}
protected internal virtual void EraseInDisplay(EraseInDisplay instruction, TContext context)
{
}
protected internal virtual void EraseInLine(EraseInLine instruction, TContext context)
{
}
protected internal virtual void PrintText(PrintText instruction, TContext context)
{
}
protected internal virtual void RestoreCursor(RestoreCursor instruction, TContext context)
{
}
protected internal virtual void SaveCursor(SaveCursor instruction, TContext context)
{
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorBack : AnsiInstruction
{
public int Count { get; }
public CursorBack(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorBack(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorDown : AnsiInstruction
{
public int Count { get; }
public CursorDown(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorDown(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorForward : AnsiInstruction
{
public int Count { get; }
public CursorForward(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorForward(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorHorizontalAbsolute : AnsiInstruction
{
public int Count { get; }
public CursorHorizontalAbsolute(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorHorizontalAbsolute(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorNextLine : AnsiInstruction
{
public int Count { get; }
public CursorNextLine(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorNextLine(this, context);
}
}
}

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

@ -0,0 +1,19 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorPosition : AnsiInstruction
{
public int Column { get; }
public int Row { get; }
public CursorPosition(int column, int row)
{
Column = column;
Row = row;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorPosition(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorPreviousLine : AnsiInstruction
{
public int Count { get; }
public CursorPreviousLine(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorPreviousLine(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class CursorUp : AnsiInstruction
{
public int Count { get; }
public CursorUp(int count)
{
Count = count;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.CursorUp(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class EraseInDisplay : AnsiInstruction
{
public int Mode { get; }
public EraseInDisplay(int mode)
{
Mode = mode;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.EraseInDisplay(this, context);
}
}
}

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

@ -0,0 +1,17 @@
namespace Spectre.Terminal.Ansi
{
public sealed class EraseInLine : AnsiInstruction
{
public int Mode { get; }
public EraseInLine(int mode)
{
Mode = mode;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.EraseInLine(this, context);
}
}
}

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

@ -0,0 +1,19 @@
using System;
namespace Spectre.Terminal.Ansi
{
public sealed class PrintText : AnsiInstruction
{
public ReadOnlyMemory<char> Text { get; }
public PrintText(ReadOnlyMemory<char> text)
{
Text = text;
}
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.PrintText(this, context);
}
}
}

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

@ -0,0 +1,10 @@
namespace Spectre.Terminal.Ansi
{
public sealed class RestoreCursor : AnsiInstruction
{
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.RestoreCursor(this, context);
}
}
}

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

@ -0,0 +1,10 @@
namespace Spectre.Terminal.Ansi
{
public sealed class SaveCursor : AnsiInstruction
{
public override void Accept<TContext>(AnsiSequenceVisitor<TContext> visitor, TContext context)
{
visitor.SaveCursor(this, context);
}
}
}

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

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Terminal.Ansi
{
internal sealed class AnsiInstructionParser
{
public static IEnumerable<AnsiInstruction> Parse(ReadOnlyMemory<char> buffer)
{
foreach (var token in AnsiInstructionTokenizer.Tokenize(buffer))
{
if (token.IsText)
{
yield return new PrintText(token.Span);
}
else
{
var instruction = ParseInstruction(token.Tokens.ToArray());
if (instruction != null)
{
yield return instruction;
}
}
}
}
private static AnsiInstruction? ParseInstruction(AnsiSequenceToken[] tokens)
{
if (tokens.Length == 0 || tokens[0].Type != AnsiSequenceTokenType.Csi)
{
return null;
}
// Get the terminal
var terminal = tokens[tokens.Length - 1].AsCharacter();
if (terminal == null)
{
return null;
}
// Get the parameters minus the terminal
var parameters = new Span<AnsiSequenceToken>(tokens)[1..^1];
// Create the instruction
return terminal.Value switch
{
'A' => ParseIntegerInstruction(parameters, count => new CursorUp(count)),
'B' => ParseIntegerInstruction(parameters, count => new CursorDown(count)),
'C' => ParseIntegerInstruction(parameters, count => new CursorForward(count)),
'D' => ParseIntegerInstruction(parameters, count => new CursorBack(count)),
'E' => ParseIntegerInstruction(parameters, count => new CursorNextLine(count)),
'F' => ParseIntegerInstruction(parameters, count => new CursorPreviousLine(count)),
'G' => ParseIntegerInstruction(parameters, count => new CursorHorizontalAbsolute(count)),
'J' => ParseIntegerInstruction(parameters, count => new EraseInDisplay(count), defaultValue: 0),
'K' => ParseIntegerInstruction(parameters, count => new EraseInLine(count), defaultValue: 0),
's' => new SaveCursor(),
'u' => new RestoreCursor(),
_ => null, // Unknown instruction
};
}
private static AnsiInstruction? ParseIntegerInstruction(ReadOnlySpan<AnsiSequenceToken> tokens, Func<int, AnsiInstruction> func, int defaultValue = 1)
{
if (tokens.Length != 1)
{
return func(defaultValue);
}
if (tokens[0].Type != AnsiSequenceTokenType.Integer)
{
return null;
}
return func(int.Parse(tokens[0].Content.Span));
}
}
}

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

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace Spectre.Terminal.Ansi
{
internal sealed class AnsiInstructionToken
{
private readonly ReadOnlyMemory<char>? _span;
private readonly IReadOnlyList<AnsiSequenceToken>? _tokens;
public ReadOnlyMemory<char> Span => _span ?? ReadOnlyMemory<char>.Empty;
public IReadOnlyList<AnsiSequenceToken> Tokens => _tokens ?? Array.Empty<AnsiSequenceToken>();
public bool IsText => _span != null;
public bool IsAnsiEscapeSequence => _tokens != null;
private AnsiInstructionToken(ReadOnlyMemory<char>? span, IReadOnlyList<AnsiSequenceToken>? tokens)
{
_span = span;
_tokens = tokens;
}
public static AnsiInstructionToken Text(ReadOnlyMemory<char> span)
{
return new AnsiInstructionToken(span, null);
}
public static AnsiInstructionToken Sequence(IReadOnlyList<AnsiSequenceToken>? tokens)
{
return new AnsiInstructionToken(null, tokens);
}
}
}

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

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Spectre.Terminal.Ansi
{
internal static class AnsiInstructionTokenizer
{
public static IReadOnlyList<AnsiInstructionToken> Tokenize(ReadOnlyMemory<char> buffer)
{
var result = new List<AnsiInstructionToken>();
foreach (var (span, isEscapeCode) in AnsiSequenceSplitter.Split(buffer))
{
if (isEscapeCode)
{
var tokens = TokenizeEscapeCode(new TextBuffer(span));
if (tokens.Count > 0)
{
result.Add(AnsiInstructionToken.Sequence(tokens));
}
}
else
{
result.Add(AnsiInstructionToken.Text(span));
}
}
return result;
}
private static IReadOnlyList<AnsiSequenceToken> TokenizeEscapeCode(TextBuffer buffer)
{
var result = new List<AnsiSequenceToken>();
while (buffer.CanRead)
{
if (!ReadEscapeCodeToken(buffer, out var token))
{
// Could not parse, so return an empty result
return Array.Empty<AnsiSequenceToken>();
}
result.Add(token);
}
return result;
}
private static bool ReadEscapeCodeToken(TextBuffer buffer, [NotNullWhen(true)] out AnsiSequenceToken? token)
{
var current = buffer.PeekChar();
// ESC?
if (current == 0x1b)
{
// 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;
}
if (char.IsNumber(current))
{
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;
}
if (char.IsLetter(current))
{
var start = buffer.Position;
buffer.Discard();
token = new AnsiSequenceToken(
AnsiSequenceTokenType.Character,
buffer.Slice(start, start + 1));
return true;
}
token = null;
return false;
}
}
public sealed class AnsiSequenceToken
{
public AnsiSequenceTokenType Type { get; }
public ReadOnlyMemory<char> Content { get; set; }
public char? AsCharacter()
{
return Content.Span[Content.Length - 1];
}
public AnsiSequenceToken(AnsiSequenceTokenType type, ReadOnlyMemory<char> value)
{
Type = type;
Content = value;
}
}
public enum AnsiSequenceTokenType
{
Unknown,
Csi,
Character,
Integer,
Semicolon,
QuestionMark,
}
}

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

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
namespace Spectre.Terminal.Ansi
{
internal static class AnsiSequenceSplitter
{
public static List<(ReadOnlyMemory<char>, bool)> Split(ReadOnlyMemory<char> buffer)
{
var index = 0;
var end = 0;
var result = new List<(ReadOnlyMemory<char>, bool)>();
while (index < buffer.Length)
{
// Encounter ESC?
if (buffer.Span[index] == 0x1b)
{
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));
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++;
}
}
}
}

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

@ -0,0 +1,69 @@
using System;
namespace Spectre.Terminal.Ansi
{
public sealed class TextBuffer
{
private readonly ReadOnlyMemory<char> _buffer;
private int _position;
public bool CanRead => _position < _buffer.Length;
public int Position => _position;
public TextBuffer(ReadOnlyMemory<char> buffer)
{
_buffer = buffer;
_position = 0;
}
public int Peek()
{
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);
}
}
}

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

@ -0,0 +1,25 @@
using System;
using Spectre.Terminal.Ansi;
namespace Spectre.Terminal
{
internal sealed class ConsoleAnsiInterpreter : AnsiSequenceVisitor<ConsoleAnsiState>
{
public static ConsoleAnsiInterpreter Instance { get; } = new ConsoleAnsiInterpreter();
public void Run(ConsoleAnsiState state, string text)
{
AnsiSequence.Interpret(this, state, text);
}
protected internal override void EraseInDisplay(EraseInDisplay instruction, ConsoleAnsiState context)
{
Console.Clear();
}
protected internal override void PrintText(PrintText instruction, ConsoleAnsiState context)
{
context.Write(instruction.Text.Span);
}
}
}

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

@ -0,0 +1,19 @@
using System;
namespace Spectre.Terminal
{
public sealed class ConsoleAnsiState
{
private readonly Action<string?> _writer;
public ConsoleAnsiState(Action<string?> writer)
{
_writer = writer;
}
public void Write(ReadOnlySpan<char> text)
{
_writer(text.ToString());
}
}
}

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

@ -0,0 +1,25 @@
using System;
namespace Spectre.Terminal
{
public sealed class FallbackTerminal : ITerminalDriver
{
public TerminalCaps Caps { get; }
public ITerminalReader Input { get; }
public ITerminalWriter Output { get; }
public ITerminalWriter Error { get; }
public FallbackTerminal()
{
Caps = new TerminalCaps()
{
Ansi = false,
};
Input = new FallbackTerminalReader();
Output = new FallbackTerminalWriter(() => Console.IsOutputRedirected, Console.Out.Write);
Error = new FallbackTerminalWriter(() => Console.IsErrorRedirected, Console.Error.Write);
}
}
}

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

@ -0,0 +1,24 @@
using System;
using System.IO;
using System.Text;
namespace Spectre.Terminal
{
internal sealed class FallbackTerminalReader : ITerminalReader
{
private readonly Stream _stream;
public Encoding Encoding => Console.OutputEncoding;
public bool IsRedirected => Console.IsInputRedirected;
public FallbackTerminalReader()
{
_stream = Console.OpenStandardInput();
}
public int Read(Span<byte> buffer)
{
return _stream.Read(buffer);
}
}
}

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

@ -0,0 +1,32 @@
using System;
using System.IO;
using System.Text;
namespace Spectre.Terminal
{
internal sealed class FallbackTerminalWriter : ITerminalWriter
{
private readonly ConsoleAnsiState _state;
private readonly Func<bool> _redirected;
public Encoding Encoding => Console.InputEncoding;
public bool IsRedirected => _redirected();
public FallbackTerminalWriter(Func<bool> redirected, Action<string?> writer)
{
_redirected = redirected;
_state = new ConsoleAnsiState(writer);
}
public void Write(ReadOnlySpan<byte> buffer)
{
var text = Encoding.GetString(buffer);
if (string.IsNullOrEmpty(text))
{
return;
}
ConsoleAnsiInterpreter.Instance.Run(_state, text);
}
}
}

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

@ -0,0 +1,11 @@
namespace Spectre.Terminal
{
public interface ITerminalDriver
{
TerminalCaps Caps { get; }
ITerminalReader Input { get; }
ITerminalWriter Output { get; }
ITerminalWriter Error { get; }
}
}

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

@ -0,0 +1,43 @@
using System;
using System.Buffers;
namespace Spectre.Terminal
{
public static class ITerminalWriterExtensions
{
public static void Write(this ITerminalWriter writer, ReadOnlySpan<char> value)
{
_ = writer ?? throw new ArgumentNullException(nameof(writer));
var len = writer.Encoding.GetByteCount(value);
var array = ArrayPool<byte>.Shared.Rent(len);
try
{
var span = array.AsSpan(0, len);
writer.Encoding.GetBytes(value, span);
writer.Write(span);
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
public static void Write(this ITerminalWriter writer, string? value)
{
Write(writer, value.AsSpan());
}
public static void WriteLine(this ITerminalWriter writer)
{
Write(writer, Environment.NewLine);
}
public static void WriteLine(this ITerminalWriter writer, string? value)
{
Write(writer, value.AsSpan());
Write(writer, Environment.NewLine);
}
}
}

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

@ -0,0 +1,12 @@
using System;
using System.Text;
namespace Spectre.Terminal
{
public interface ITerminalReader
{
Encoding Encoding { get; }
bool IsRedirected { get; }
int Read(Span<byte> buffer);
}
}

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

@ -0,0 +1,12 @@
using System;
using System.Text;
namespace Spectre.Terminal
{
public interface ITerminalWriter
{
Encoding Encoding { get; }
bool IsRedirected { get; }
void Write(ReadOnlySpan<byte> buffer);
}
}

33
src/Terminal/Terminal.cs Normal file
Просмотреть файл

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spectre.Terminal
{
public sealed class Terminal
{
public TerminalInput Input { get; }
public TerminalOutput Output { get; }
public TerminalOutput Error { get; }
public Terminal(ITerminalDriver driver)
{
if (driver is null)
{
throw new ArgumentNullException(nameof(driver));
}
Input = new TerminalInput(driver.Input);
Output = new TerminalOutput(driver.Output);
Error = new TerminalOutput(driver.Error);
}
public static Terminal Create()
{
// Use the fallback terminal for now
return new Terminal(new FallbackTerminal());
}
}
}

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

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

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

@ -0,0 +1,7 @@
namespace Spectre.Terminal
{
public sealed class TerminalCaps
{
public bool Ansi { get; init; }
}
}

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

@ -0,0 +1,68 @@
using System;
using System.Text;
namespace Spectre.Terminal
{
public sealed class TerminalInput : ITerminalReader
{
private readonly ITerminalReader _reader;
private readonly object _lock;
private ITerminalReader? _redirected;
public Encoding Encoding => GetEncoding();
public bool IsRedirected => GetIsRedirected();
public TerminalInput(ITerminalReader reader)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_lock = new object();
}
public void Redirect(ITerminalReader? reader)
{
lock (_lock)
{
_redirected = reader;
}
}
public int Read(Span<byte> buffer)
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.Read(buffer);
}
return _reader.Read(buffer);
}
}
private Encoding GetEncoding()
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.Encoding;
}
return _reader.Encoding;
}
}
private bool GetIsRedirected()
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.IsRedirected;
}
return _reader.IsRedirected;
}
}
}
}

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

@ -0,0 +1,70 @@
using System;
using System.Text;
namespace Spectre.Terminal
{
public sealed class TerminalOutput : ITerminalWriter
{
private readonly ITerminalWriter _writer;
private readonly object _lock;
private ITerminalWriter? _redirected;
public Encoding Encoding => GetEncoding();
public bool IsRedirected => GetIsRedirected();
public TerminalOutput(ITerminalWriter reader)
{
_writer = reader ?? throw new ArgumentNullException(nameof(reader));
_lock = new object();
}
public void Redirect(ITerminalWriter? writer)
{
lock (_lock)
{
_redirected = writer;
}
}
public void Write(ReadOnlySpan<byte> buffer)
{
lock (_lock)
{
if (_redirected != null)
{
_redirected.Write(buffer);
}
else
{
_writer.Write(buffer);
}
}
}
private Encoding GetEncoding()
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.Encoding;
}
return _writer.Encoding;
}
}
private bool GetIsRedirected()
{
lock (_lock)
{
if (_redirected != null)
{
return _redirected.IsRedirected;
}
return _writer.IsRedirected;
}
}
}
}