Compare commits

...

20 Commits

Author SHA1 Message Date
Patrik Svensson
1d74fb909c Add support for adding empty rows
This affects grids and tables.
2020-08-11 17:49:28 +02:00
Patrik Svensson
5d132220ba Enable nullable reference types
Closes #36
2020-08-11 17:24:34 +02:00
Patrik Svensson
a273f74758 Add aliases for styles
Closes #37
2020-08-11 16:45:05 +02:00
Patrik Svensson
717931f11c Add support for RGB colors
Closes #34
2020-08-11 16:45:05 +02:00
Patrik Svensson
bcfc495843 Add support for hex colors
Closes #33
2020-08-11 16:45:05 +02:00
Patrik Svensson
9aa36c4cf0 Do not include cell separators in grid
Closes #40
2020-08-11 11:53:34 +02:00
Patrik Svensson
22d4af4482 Preserve line breaks 2020-08-10 11:42:34 +02:00
Patrik Svensson
f4d1796e40 Do not split lines if width is 0
Closes #32
2020-08-09 11:53:56 +02:00
Patrik Svensson
2dd0eb9f74 Add support for column alignment and padding
Closes #12
Closes #31
2020-08-08 23:11:34 +02:00
Patrik Svensson
fa85216554 Add fallback for unicode borders 2020-08-07 22:24:38 +02:00
Patrik Svensson
d475e3b30a Reset colors before line break
Closes #28
2020-08-07 13:12:03 +02:00
Patrik Svensson
9637066927 Add better algorithm for calculating column widths
Closes #14
2020-08-07 12:55:33 +02:00
AdmiringWorm
0b4321115a Add background color examples in Sample console 2020-08-07 08:20:20 +02:00
AdmiringWorm
5cd9ece31a Add check for IsDefault when comparing Colors 2020-08-07 08:20:20 +02:00
AdmiringWorm
b0341862cf Add failing unit test for comparing black and default color 2020-08-07 08:20:20 +02:00
AdmiringWorm
2e7b3d520a Update regex to correctly identify Windows 10
When running .NET Core 2.1 or .NET Framework 4.6.1, the used regex for
detecting Windows 10 TrueColor support does not match the OSDescription.
The reason for this is because on those frameworks the description has
trailing whitespaces, but they do not on .NET Core 3.1.
This commit updates the regex to ignore any trailing whitespaces.
2020-08-07 08:11:34 +02:00
Patrik Svensson
646f51a628 Fix NuGet badge (skip-ci) 2020-08-05 16:03:09 +02:00
Patrik Svensson
a0bd481255 Add package icon
Closes #20
2020-08-05 15:49:43 +02:00
Patrik Svensson
6d197c5140 Add border support for panels
Closes #11
2020-08-05 15:28:15 +02:00
Patrik Svensson
108e56c229 Add rounded border 2020-08-05 14:19:45 +02:00
54 changed files with 1836 additions and 410 deletions

View File

@@ -1,6 +1,6 @@
# `Spectre.Console`
_[![Spectre.IO NuGet Version](https://img.shields.io/nuget/v/spectre.io.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_
_[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_
A .NET Standard 2.0 library that makes it easier to create beautiful console applications.
It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich)

BIN
gfx/large-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

BIN
gfx/medium-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
gfx/small-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -77,4 +77,7 @@ dotnet_diagnostic.CA1032.severity = none
dotnet_diagnostic.CA1826.severity = none
# RCS1079: Throwing of new NotImplementedException.
dotnet_diagnostic.RCS1079.severity = warning
dotnet_diagnostic.RCS1079.severity = warning
# RCS1057: Add empty line between declarations.
dotnet_diagnostic.RCS1057.severity = none

View File

@@ -18,6 +18,7 @@
<Authors>Patrik Svensson</Authors>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/spectresystems/spectre.console</RepositoryUrl>
<PackageIcon>small-logo.png</PackageIcon>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/spectresystems/spectre.console</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -33,7 +34,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="MinVer" PrivateAssets="All" Version="2.3.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -13,15 +13,22 @@ namespace Sample
AnsiConsole.WriteLine("Hello World!");
AnsiConsole.Reset();
AnsiConsole.MarkupLine("Capabilities: [yellow underline]{0}[/]", AnsiConsole.Capabilities);
AnsiConsole.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName);
AnsiConsole.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", AnsiConsole.Width, AnsiConsole.Height);
AnsiConsole.MarkupLine("[white on red]Good[/] [red]bye[/]!");
AnsiConsole.WriteLine();
// We can also use System.ConsoleColor with AnsiConsole.
// We can also use System.ConsoleColor with AnsiConsole
// to set the foreground and background color.
foreach (ConsoleColor value in Enum.GetValues(typeof(ConsoleColor)))
{
AnsiConsole.Foreground = value;
AnsiConsole.WriteLine("ConsoleColor.{0}", value);
var foreground = value;
var background = (ConsoleColor)(15 - (int)value);
AnsiConsole.Foreground = foreground;
AnsiConsole.Background = background;
AnsiConsole.WriteLine("{0} on {1}", foreground, background);
AnsiConsole.ResetColors();
}
// We can get the default console via the static API.
@@ -45,40 +52,55 @@ namespace Sample
console.ResetColors();
console.ResetDecoration();
console.MarkupLine("Capabilities: [yellow underline]{0}[/]", console.Capabilities);
console.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName);
console.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", console.Width, console.Height);
console.MarkupLine("[white on red]Good[/] [red]bye[/]!");
console.WriteLine();
// Nest some panels and text
AnsiConsole.Foreground = Color.Maroon;
AnsiConsole.Render(new Panel(new Panel(new Panel(new Panel(
Text.New(
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
"So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" +
"😅",
foreground: Color.White), content: Justify.Center)))));
AnsiConsole.Render(
new Panel(
new Panel(
new Panel(
new Panel(
Text.New(
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
"So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" +
"😅", foreground: Color.White))
{ Alignment = Justify.Center, Border = BorderKind.Rounded })))
{
Border = BorderKind.Ascii
});
// Reset colors
AnsiConsole.ResetColors();
// Left adjusted panel with text
AnsiConsole.Render(new Panel(
Text.New("Left adjusted\nLeft",
foreground: Color.White),
fit: true));
Text.New("Left adjusted\nLeft"))
{
Expand = true,
Alignment = Justify.Left,
});
// Centered panel with text
AnsiConsole.Render(new Panel(
Text.New("Centered\nCenter",
foreground: Color.White),
fit: true, content: Justify.Center));
Text.New("Centered\nCenter"))
{
Expand = true,
Alignment = Justify.Center,
});
// Right adjusted panel with text
AnsiConsole.Render(new Panel(
Text.New("Right adjusted\nRight",
foreground: Color.White),
fit: true, content: Justify.Right));
Text.New("Right adjusted\nRight"))
{
Expand = true,
Alignment = Justify.Right,
});
// A normal, square table
var table = new Table();
table.AddColumns("[red underline]Foo[/]", "Bar");
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
@@ -87,7 +109,8 @@ namespace Sample
table.AddRow("Hej 👋", "[green]Världen[/]");
AnsiConsole.Render(table);
table = new Table(BorderKind.Ascii);
// A rounded table
table = new Table { Border = BorderKind.Rounded };
table.AddColumns("[red underline]Foo[/]", "Bar");
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
@@ -95,16 +118,52 @@ namespace Sample
table.AddRow("Hej 👋", "[green]Världen[/]");
AnsiConsole.Render(table);
// A rounded table without headers
table = new Table { Border = BorderKind.Rounded, ShowHeaders = false };
table.AddColumns("[red underline]Foo[/]", "Bar");
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
table.AddRow("Lorem ipsum dolor sit amet, consectetur [blue]adipiscing[/] elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "◀ Strange language");
table.AddRow("Hej 👋", "[green]Världen[/]");
AnsiConsole.Render(table);
// Emulate the usage information for "dotnet run"
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine(" Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] <additional arguments>...]][/]");
AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] <additional arguments>...]][/]");
AnsiConsole.WriteLine();
var grid = new Grid();
grid.AddColumns(3);
grid.AddRow(" Options", "", "");
grid.AddRow(" [blue]-h[/], [blue]--help[/]", " ", "Show command line help.");
grid.AddRow(" [blue]-c[/], [blue]--configuration[/] <CONFIGURATION>", " ", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] <LEVEL>", " ", "Set the MSBuild verbosity level.\nAllowed values are q[grey][[uiet][/], m[grey][[inimal][/], n[grey][[ormal][/], d[grey][[etailed][/], and diag[grey][[nostic][/].");
grid.AddColumn(new GridColumn { NoWrap = true });
grid.AddColumn(new GridColumn { NoWrap = true, Width = 2 });
grid.AddColumn();
grid.AddRow("Options:", "", "");
grid.AddRow(" [blue]-h[/], [blue]--help[/]", "", "Show command line help.");
grid.AddRow(" [blue]-c[/], [blue]--configuration[/] <CONFIGURATION>", "", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] <LEVEL>", "", "Set the MSBuild verbosity level. Allowed values are \nq[grey][[uiet][/], m[grey][[inimal][/], n[grey][[ormal][/], d[grey][[etailed][/], and diag[grey][[nostic][/].");
AnsiConsole.Render(grid);
// A simple table
AnsiConsole.WriteLine();
table = new Table { Border = BorderKind.Rounded };
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddColumn("Baz");
table.AddRow("Qux\nQuuuuuux", "[blue]Corgi[/]", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
AnsiConsole.Render(table);
// Render a table in some panels.
AnsiConsole.Render(new Panel(new Panel(table) { Border = BorderKind.Ascii }) { Padding = new Padding(0, 0) });
// Draw another table
table = new Table { Expand = false };
table.AddColumn(new TableColumn("Date"));
table.AddColumn(new TableColumn("Title"));
table.AddColumn(new TableColumn("Production\nBudget"));
table.AddColumn(new TableColumn("Box Office"));
table.AddRow("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "[red]$375,126,118[/]");
table.AddRow("May 25, 2018", "[yellow]Solo[/]: A Star Wars Story", "$275,000,000", "$393,151,347");
table.AddRow("Dec 15, 2017", "Star Wars Ep. VIII: The Last Jedi", "$262,000,000", "[bold green]$1,332,539,889[/]");
AnsiConsole.Render(table);
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Spectre.Console.Tests
{
public sealed class PlainConsole : IAnsiConsole, IDisposable
{
public Capabilities Capabilities => throw new NotSupportedException();
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
public int Width { get; }
@@ -18,14 +18,19 @@ namespace Spectre.Console.Tests
public Color Background { get; set; }
public StringWriter Writer { get; }
public string RawOutput => Writer.ToString();
public string Output => Writer.ToString().TrimEnd('\n');
public IReadOnlyList<string> Lines => Output.Split(new char[] { '\n' });
public PlainConsole(int width = 80, int height = 9000, Encoding encoding = null)
public PlainConsole(
int width = 80, int height = 9000, Encoding encoding = null,
bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard,
bool legacyConsole = false)
{
Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole);
Encoding = encoding ?? Encoding.UTF8;
Width = width;
Height = height;
Encoding = encoding ?? Encoding.UTF8;
Writer = new StringWriter();
}

View File

@@ -7,11 +7,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="Shouldly" Version="4.0.0-beta0002" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -246,6 +246,38 @@ namespace Spectre.Console.Tests.Unit
public sealed class WriteLine
{
[Fact]
public void Should_Reset_Colors_Correctly_After_Line_Break()
{
// Given
var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes);
// When
fixture.Console.Background = ConsoleColor.Red;
fixture.Console.WriteLine("Hello");
fixture.Console.Background = ConsoleColor.Green;
fixture.Console.WriteLine("World");
// Then
fixture.Output.NormalizeLineEndings()
.ShouldBe("Hello\nWorld\n");
}
[Fact]
public void Should_Reset_Colors_Correctly_After_Line_Break_In_Text()
{
// Given
var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes);
// When
fixture.Console.Background = ConsoleColor.Red;
fixture.Console.WriteLine("Hello\nWorld");
// Then
fixture.Output.NormalizeLineEndings()
.ShouldBe("Hello\nWorld\n");
}
[Theory]
[InlineData(AnsiSupport.Yes)]
[InlineData(AnsiSupport.No)]

View File

@@ -78,6 +78,20 @@ namespace Spectre.Console.Tests.Unit
// Then
result.ShouldBeFalse();
}
[Fact]
public void Shourd_Not_Consider_Black_And_Default_Colors_Equal()
{
// Given
var color1 = Color.Default;
var color2 = Color.Black;
// When
var result = color1.Equals(color2);
// Then
result.ShouldBeFalse();
}
}
public sealed class TheGetHashCodeMethod

View File

@@ -10,12 +10,18 @@ namespace Spectre.Console.Tests.Unit.Composition
public sealed class TheGetBorderMethod
{
[Theory]
[InlineData(BorderKind.Ascii, typeof(AsciiBorder))]
[InlineData(BorderKind.Square, typeof(SquareBorder))]
public void Should_Return_Correct_Border_For_Specified_Kind(BorderKind kind, Type expected)
[InlineData(BorderKind.None, false, typeof(NoBorder))]
[InlineData(BorderKind.Ascii, false, typeof(AsciiBorder))]
[InlineData(BorderKind.Square, false, typeof(SquareBorder))]
[InlineData(BorderKind.Rounded, false, typeof(RoundedBorder))]
[InlineData(BorderKind.None, true, typeof(NoBorder))]
[InlineData(BorderKind.Ascii, true, typeof(AsciiBorder))]
[InlineData(BorderKind.Square, true, typeof(SquareBorder))]
[InlineData(BorderKind.Rounded, true, typeof(SquareBorder))]
public void Should_Return_Correct_Border_For_Specified_Kind(BorderKind kind, bool safe, Type expected)
{
// Given, When
var result = Border.GetBorder(kind);
var result = Border.GetBorder(kind, safe);
// Then
result.ShouldBeOfType(expected);
@@ -25,7 +31,7 @@ namespace Spectre.Console.Tests.Unit.Composition
public void Should_Throw_If_Unknown_Border_Kind_Is_Specified()
{
// Given, When
var result = Record.Exception(() => Border.GetBorder((BorderKind)int.MaxValue));
var result = Record.Exception(() => Border.GetBorder((BorderKind)int.MaxValue, false));
// Then
result.ShouldBeOfType<InvalidOperationException>();

View File

@@ -6,6 +6,25 @@ namespace Spectre.Console.Tests.Unit.Composition
{
public sealed class GridTests
{
public sealed class TheAddColumnMethod
{
[Fact]
public void Should_Throw_If_Rows_Are_Not_Empty()
{
// Given
var grid = new Grid();
grid.AddColumn();
grid.AddRow("Hello World!");
// When
var result = Record.Exception(() => grid.AddColumn());
// Then
result.ShouldBeOfType<InvalidOperationException>()
.Message.ShouldBe("Cannot add new columns to grid with existing rows.");
}
}
public sealed class TheAddRowMethod
{
[Fact]
@@ -27,7 +46,8 @@ namespace Spectre.Console.Tests.Unit.Composition
{
// Given
var grid = new Grid();
grid.AddColumns(2);
grid.AddColumn();
grid.AddColumn();
// When
var result = Record.Exception(() => grid.AddRow("Foo"));
@@ -53,13 +73,39 @@ namespace Spectre.Console.Tests.Unit.Composition
}
}
public sealed class TheAddEmptyRowMethod
{
[Fact]
public void Should_Add_Empty_Row()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumns(2);
grid.AddRow("Foo", "Bar");
grid.AddEmptyRow();
grid.AddRow("Qux", "Corgi");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("Foo Bar ");
console.Lines[1].ShouldBe(" ");
console.Lines[2].ShouldBe("Qux Corgi");
}
}
[Fact]
public void Should_Render_Grid_With_No_Border_Correctly()
public void Should_Render_Grid_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumns(3);
grid.AddColumn();
grid.AddColumn();
grid.AddColumn();
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
@@ -73,14 +119,82 @@ namespace Spectre.Console.Tests.Unit.Composition
}
[Fact]
public void Should_Render_Grid()
public void Should_Render_Grid_Column_Alignment_Correctly()
{
var console = new PlainConsole(width: 120);
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { Alignment = Justify.Right });
grid.AddColumn(new GridColumn { Alignment = Justify.Center });
grid.AddColumn(new GridColumn { Alignment = Justify.Left });
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe(" Foo Bar Baz ");
console.Lines[1].ShouldBe(" Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred ");
}
[Fact]
public void Should_Use_Default_Padding()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumns(3);
grid.AddRow("[bold]Options[/]", string.Empty, string.Empty);
grid.AddRow(" [blue]-h[/], [blue]--help[/]", " ", "Show command line help.");
grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", " ", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("Foo Bar Baz ");
console.Lines[1].ShouldBe("Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred ");
}
[Fact]
public void Should_Render_Explicit_Grid_Column_Padding_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(3, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
grid.AddColumn(new GridColumn { Padding = new Padding(0, 3) });
grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred");
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe(" Foo Bar Baz ");
console.Lines[1].ShouldBe(" Qux Corgi Waldo ");
console.Lines[2].ShouldBe(" GraultGarplyFred ");
}
[Fact]
public void Should_Render_Grid()
{
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumn(new GridColumn { NoWrap = true });
grid.AddColumn(new GridColumn { Padding = new Padding(2, 0) });
grid.AddRow("[bold]Options[/]", string.Empty);
grid.AddRow(" [blue]-h[/], [blue]--help[/]", "Show command line help.");
grid.AddRow(" [blue]-c[/], [blue]--configuration[/]", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
// When
console.Render(grid);

View File

@@ -21,6 +21,25 @@ namespace Spectre.Console.Tests.Unit
console.Lines[2].ShouldBe("└─────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Padding()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel(Text.New("Hello World"))
{
Padding = new Padding(3, 5),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└───────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Unicode_Correctly()
{
@@ -62,8 +81,7 @@ namespace Spectre.Console.Tests.Unit
// Given
var console = new PlainConsole(width: 80);
var text = new Panel(
Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"),
content: Justify.Center);
Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
// When
console.Render(text);
@@ -71,7 +89,7 @@ namespace Spectre.Console.Tests.Unit
// Then
console.Lines.Count.ShouldBe(7);
console.Lines[0].ShouldBe("┌───────────────────────┐");
console.Lines[1].ShouldBe("│ I heard you like 📦 │");
console.Lines[1].ShouldBe("│ I heard you like 📦 │");
console.Lines[2].ShouldBe("│ │");
console.Lines[3].ShouldBe("│ │");
console.Lines[4].ShouldBe("│ │");
@@ -80,19 +98,23 @@ namespace Spectre.Console.Tests.Unit
}
[Fact]
public void Should_Fit_Panel_To_Parent_If_Enabled()
public void Should_Expand_Panel_If_Enabled()
{
// Given
var console = new PlainConsole(width: 25);
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel(Text.New("Hello World"), fit: true));
console.Render(new Panel(Text.New("Hello World"))
{
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└───────────────────────┘");
console.Lines[0].Length.ShouldBe(80);
console.Lines[0].ShouldBe("┌──────────────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
@@ -102,7 +124,12 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 25);
// When
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Right));
console.Render(
new Panel(
Text.New("Hello World").WithAlignment(Justify.Right))
{
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
@@ -118,7 +145,12 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 25);
// When
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Center));
console.Render(
new Panel(
Text.New("Hello World").WithAlignment(Justify.Center))
{
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);

View File

@@ -15,12 +15,28 @@ namespace Spectre.Console.Tests.Unit.Composition
var table = new Table();
// When
var result = Record.Exception(() => table.AddColumn(null));
var result = Record.Exception(() => table.AddColumn((string)null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("column");
}
[Fact]
public void Should_Throw_If_Rows_Are_Not_Empty()
{
// Given
var grid = new Table();
grid.AddColumn("Foo");
grid.AddRow("Hello World");
// When
var result = Record.Exception(() => grid.AddColumn("Bar"));
// Then
result.ShouldBeOfType<InvalidOperationException>()
.Message.ShouldBe("Cannot add new columns to table with existing rows.");
}
}
public sealed class TheAddColumnsMethod
@@ -32,7 +48,7 @@ namespace Spectre.Console.Tests.Unit.Composition
var table = new Table();
// When
var result = Record.Exception(() => table.AddColumns(null));
var result = Record.Exception(() => table.AddColumns((string[])null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
@@ -88,6 +104,34 @@ namespace Spectre.Console.Tests.Unit.Composition
}
}
public sealed class TheAddEmptyRowMethod
{
[Fact]
public void Should_Render_Table_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddEmptyRow();
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(7);
console.Lines[0].ShouldBe("┌────────┬────────┬───────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ │ │ │");
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴───────┘");
}
}
[Fact]
public void Should_Render_Table_Correctly()
{
@@ -102,6 +146,7 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("┌────────┬────────┬───────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
@@ -111,11 +156,69 @@ namespace Spectre.Console.Tests.Unit.Composition
}
[Fact]
public void Should_Render_Table_With_Specified_Border_Correctly()
public void Should_Render_Table_Nested_In_Panels_Correctly()
{
// A simple table
var console = new PlainConsole(width: 80);
var table = new Table() { Border = BorderKind.Rounded };
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddColumn(new TableColumn("Baz") { Alignment = Justify.Right });
table.AddRow("Qux\nQuuuuuux", "[blue]Corgi[/]", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// Render a table in some panels.
console.Render(new Panel(new Panel(table)
{
Border = BorderKind.Ascii,
}));
// Then
console.Lines.Count.ShouldBe(11);
console.Lines[00].ShouldBe("┌───────────────────────────────────┐");
console.Lines[01].ShouldBe("│ +-------------------------------+ │");
console.Lines[02].ShouldBe("│ | ╭──────────┬────────┬───────╮ | │");
console.Lines[03].ShouldBe("│ | │ Foo │ Bar │ Baz │ | │");
console.Lines[04].ShouldBe("│ | ├──────────┼────────┼───────┤ | │");
console.Lines[05].ShouldBe("│ | │ Qux │ Corgi │ Waldo │ | │");
console.Lines[06].ShouldBe("│ | │ Quuuuuux │ │ │ | │");
console.Lines[07].ShouldBe("│ | │ Grault │ Garply │ Fred │ | │");
console.Lines[08].ShouldBe("│ | ╰──────────┴────────┴───────╯ | │");
console.Lines[09].ShouldBe("│ +-------------------------------+ │");
console.Lines[10].ShouldBe("└───────────────────────────────────┘");
}
[Fact]
public void Should_Render_Table_With_Column_Justification_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table(BorderKind.Ascii);
var table = new Table();
table.AddColumn(new TableColumn("Foo") { Alignment = Justify.Left });
table.AddColumn(new TableColumn("Bar") { Alignment = Justify.Right });
table.AddColumn(new TableColumn("Baz") { Alignment = Justify.Center });
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Lorem ipsum dolor sit amet");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("┌────────┬────────┬────────────────────────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼────────────────────────────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Grault │ Garply │ Lorem ipsum dolor sit amet │");
console.Lines[5].ShouldBe("└────────┴────────┴────────────────────────────┘");
}
[Fact]
public void Should_Expand_Table_To_Available_Space_If_Specified()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table() { Expand = true };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
@@ -124,6 +227,31 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].Length.ShouldBe(80);
console.Lines[0].ShouldBe("┌───────────────────────────┬───────────────────────────┬──────────────────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├───────────────────────────┼───────────────────────────┼──────────────────────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[5].ShouldBe("└───────────────────────────┴───────────────────────────┴──────────────────────┘");
}
[Fact]
public void Should_Render_Table_With_Ascii_Border_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = BorderKind.Ascii };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("+-------------------------+");
console.Lines[1].ShouldBe("| Foo | Bar | Baz |");
console.Lines[2].ShouldBe("|--------+--------+-------|");
@@ -132,12 +260,35 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[5].ShouldBe("+-------------------------+");
}
[Fact]
public void Should_Render_Table_With_Rounded_Border_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = BorderKind.Rounded };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(6);
console.Lines[0].ShouldBe("╭────────┬────────┬───────╮");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[5].ShouldBe("╰────────┴────────┴───────╯");
}
[Fact]
public void Should_Render_Table_With_No_Border_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table(BorderKind.None);
var table = new Table { Border = BorderKind.None };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
@@ -147,9 +298,9 @@ namespace Spectre.Console.Tests.Unit.Composition
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("Foo Bar Baz ");
console.Lines[1].ShouldBe("Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred ");
console.Lines[0].ShouldBe("Foo Bar Baz ");
console.Lines[1].ShouldBe("Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred ");
}
[Fact]
@@ -166,6 +317,7 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Render(table);
// Then
console.Lines.Count.ShouldBe(7);
console.Lines[0].ShouldBe("┌────────┬────────┬───────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
@@ -174,5 +326,49 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴───────┘");
}
[Fact]
public void Should_Render_Table_With_Cell_Padding_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar");
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 2) });
table.AddRow("Qux\nQuuux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(7);
console.Lines[0].ShouldBe("┌────────┬────────┬──────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("├────────┼────────┼──────────┤");
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
console.Lines[4].ShouldBe("│ Quuux │ │ │");
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴──────────┘");
}
[Fact]
public void Should_Render_Table_Without_Footer_If_No_Rows_Are_Added()
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table();
table.AddColumns("Foo", "Bar");
table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 2) });
// When
console.Render(table);
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─────┬─────┬────────┐");
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
console.Lines[2].ShouldBe("└─────┴─────┴────────┘");
}
}
}

View File

@@ -21,6 +21,20 @@ namespace Spectre.Console.Tests.Unit
.ShouldBe("Hello World");
}
[Fact]
public void Should_Write_Line_Breaks()
{
// Given
var fixture = new PlainConsole(width: 5);
var text = Text.New("\n\n");
// When
fixture.Render(text);
// Then
fixture.RawOutput.ShouldBe("\n\n");
}
[Fact]
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width()
{

View File

@@ -39,14 +39,18 @@ namespace Spectre.Console.Tests.Unit
[Theory]
[InlineData("bold", Decoration.Bold)]
[InlineData("b", Decoration.Bold)]
[InlineData("dim", Decoration.Dim)]
[InlineData("i", Decoration.Italic)]
[InlineData("italic", Decoration.Italic)]
[InlineData("underline", Decoration.Underline)]
[InlineData("u", Decoration.Underline)]
[InlineData("invert", Decoration.Invert)]
[InlineData("conceal", Decoration.Conceal)]
[InlineData("slowblink", Decoration.SlowBlink)]
[InlineData("rapidblink", Decoration.RapidBlink)]
[InlineData("strikethrough", Decoration.Strikethrough)]
[InlineData("s", Decoration.Strikethrough)]
public void Should_Parse_Decoration(string text, Decoration decoration)
{
// Given, When
@@ -126,108 +130,83 @@ namespace Spectre.Console.Tests.Unit
result.ShouldBeOfType<InvalidOperationException>();
result.Message.ShouldBe("Could not find color 'lol'.");
}
[Theory]
[InlineData("#FF0000 on #0000FF")]
[InlineData("#F00 on #00F")]
public void Should_Parse_Hex_Colors_Correctly(string style)
{
// Given, When
var result = Style.Parse(style);
// Then
result.Foreground.ShouldBe(Color.Red);
result.Background.ShouldBe(Color.Blue);
}
[Theory]
[InlineData("#", "Invalid hex color '#'.")]
[InlineData("#FF00FF00FF", "Invalid hex color '#FF00FF00FF'.")]
[InlineData("#FOO", "Invalid hex color '#FOO'. Could not find any recognizable digits.")]
public void Should_Return_Error_If_Hex_Color_Is_Invalid(string style, string expected)
{
// Given, When
var result = Record.Exception(() => Style.Parse(style));
// Then
result.ShouldNotBeNull();
result.Message.ShouldBe(expected);
}
[Theory]
[InlineData("rgb(255,0,0) on rgb(0,0,255)")]
public void Should_Parse_Rgb_Colors_Correctly(string style)
{
// Given, When
var result = Style.Parse(style);
// Then
result.Foreground.ShouldBe(Color.Red);
result.Background.ShouldBe(Color.Blue);
}
[Theory]
[InlineData("rgb()", "Invalid RGB color 'rgb()'.")]
[InlineData("rgb(", "Invalid RGB color 'rgb('.")]
[InlineData("rgb(255)", "Invalid RGB color 'rgb(255)'.")]
[InlineData("rgb(255,255)", "Invalid RGB color 'rgb(255,255)'.")]
[InlineData("rgb(255,255,255", "Invalid RGB color 'rgb(255,255,255'.")]
[InlineData("rgb(A,B,C)", "Invalid RGB color 'rgb(A,B,C)'. Input string was not in a correct format.")]
public void Should_Return_Error_If_Rgb_Color_Is_Invalid(string style, string expected)
{
// Given, When
var result = Record.Exception(() => Style.Parse(style));
// Then
result.ShouldNotBeNull();
result.Message.ShouldBe(expected);
}
}
public sealed class TheTryParseMethod
{
[Fact]
public void Default_Keyword_Should_Return_Default_Style()
public void Should_Return_True_If_Parsing_Succeeded()
{
// Given, When
var result = Style.TryParse("default", out var style);
var result = Style.TryParse("bold", out var style);
// Then
result.ShouldBeTrue();
style.ShouldNotBeNull();
style.Foreground.ShouldBe(Color.Default);
style.Background.ShouldBe(Color.Default);
style.Decoration.ShouldBe(Decoration.None);
}
[Theory]
[InlineData("bold", Decoration.Bold)]
[InlineData("dim", Decoration.Dim)]
[InlineData("italic", Decoration.Italic)]
[InlineData("underline", Decoration.Underline)]
[InlineData("invert", Decoration.Invert)]
[InlineData("conceal", Decoration.Conceal)]
[InlineData("slowblink", Decoration.SlowBlink)]
[InlineData("rapidblink", Decoration.RapidBlink)]
[InlineData("strikethrough", Decoration.Strikethrough)]
public void Should_Parse_Decoration(string text, Decoration decoration)
{
// Given, When
var result = Style.TryParse(text, out var style);
// Then
result.ShouldBeTrue();
style.ShouldNotBeNull();
style.Decoration.ShouldBe(decoration);
style.Decoration.ShouldBe(Decoration.Bold);
}
[Fact]
public void Should_Parse_Text_And_Decoration()
public void Should_Return_False_If_Parsing_Failed()
{
// Given, When
var result = Style.TryParse("bold underline blue on green", out var style);
// Then
result.ShouldBeTrue();
style.ShouldNotBeNull();
style.Decoration.ShouldBe(Decoration.Bold | Decoration.Underline);
style.Foreground.ShouldBe(Color.Blue);
style.Background.ShouldBe(Color.Green);
}
[Fact]
public void Should_Parse_Background_If_Foreground_Is_Set_To_Default()
{
// Given, When
var result = Style.TryParse("default on green", out var style);
// Then
result.ShouldBeTrue();
style.ShouldNotBeNull();
style.Decoration.ShouldBe(Decoration.None);
style.Foreground.ShouldBe(Color.Default);
style.Background.ShouldBe(Color.Green);
}
[Fact]
public void Should_Throw_If_Foreground_Is_Set_Twice()
{
// Given, When
var result = Style.TryParse("green yellow", out _);
// Then
result.ShouldBeFalse();
}
[Fact]
public void Should_Throw_If_Background_Is_Set_Twice()
{
// Given, When
var result = Style.TryParse("green on blue yellow", out _);
// Then
result.ShouldBeFalse();
}
[Fact]
public void Should_Throw_If_Color_Name_Could_Not_Be_Found()
{
// Given, When
var result = Style.TryParse("bold lol", out _);
// Then
result.ShouldBeFalse();
}
[Fact]
public void Should_Throw_If_Background_Color_Name_Could_Not_Be_Found()
{
// Given, When
var result = Style.TryParse("blue on lol", out _);
var result = Style.TryParse("lol", out _);
// Then
result.ShouldBeFalse();

View File

@@ -21,6 +21,6 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the out buffer.
/// </summary>
public TextWriter Out { get; set; }
public TextWriter? Out { get; set; }
}
}

View File

@@ -25,7 +25,13 @@ namespace Spectre.Console
/// </remarks>
public bool LegacyConsole { get; }
internal Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole)
/// <summary>
/// Initializes a new instance of the <see cref="Capabilities"/> class.
/// </summary>
/// <param name="supportsAnsi">Whether or not ANSI escape sequences are supported.</param>
/// <param name="colorSystem">The color system that is supported.</param>
/// <param name="legacyConsole">Whether or not this is a legacy console.</param>
public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole)
{
SupportsAnsi = supportsAnsi;
ColorSystem = colorSystem;
@@ -44,7 +50,7 @@ namespace Spectre.Console
ColorSystem.Standard => "4 bits",
ColorSystem.EightBit => "8 bits",
ColorSystem.TrueColor => "24 bits",
_ => "?"
_ => "?",
};
return $"ANSI={supportsAnsi}, Colors={ColorSystem}, Kind={legacyConsole} ({bits})";

View File

@@ -74,7 +74,7 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is Color color && Equals(color);
}
@@ -82,7 +82,8 @@ namespace Spectre.Console
/// <inheritdoc/>
public bool Equals(Color other)
{
return R == other.R && G == other.G && B == other.B;
return (IsDefault && other.IsDefault) ||
(IsDefault == other.IsDefault && R == other.R && G == other.G && B == other.B);
}
/// <summary>

View File

@@ -17,6 +17,12 @@ namespace Spectre.Console.Composition
{ BorderKind.None, new NoBorder() },
{ BorderKind.Ascii, new AsciiBorder() },
{ BorderKind.Square, new SquareBorder() },
{ BorderKind.Rounded, new RoundedBorder() },
};
private static readonly Dictionary<BorderKind, BorderKind> _safeLookup = new Dictionary<BorderKind, BorderKind>
{
{ BorderKind.Rounded, BorderKind.Square },
};
/// <summary>
@@ -31,9 +37,15 @@ namespace Spectre.Console.Composition
/// Gets a <see cref="Border"/> represented by the specified <see cref="BorderKind"/>.
/// </summary>
/// <param name="kind">The kind of border to get.</param>
/// <param name="safe">Whether or not to get a "safe" border that can be rendered in a legacy console.</param>
/// <returns>A <see cref="Border"/> instance representing the specified <see cref="BorderKind"/>.</returns>
public static Border GetBorder(BorderKind kind)
public static Border GetBorder(BorderKind kind, bool safe)
{
if (safe && _safeLookup.TryGetValue(kind, out var safeKind))
{
kind = safeKind;
}
if (!_borders.TryGetValue(kind, out var border))
{
throw new InvalidOperationException("Unknown border kind");
@@ -45,15 +57,20 @@ namespace Spectre.Console.Composition
private Dictionary<BorderPart, string> Initialize()
{
var lookup = new Dictionary<BorderPart, string>();
foreach (BorderPart part in Enum.GetValues(typeof(BorderPart)))
foreach (BorderPart? part in Enum.GetValues(typeof(BorderPart)))
{
var text = GetBoxPart(part);
if (part == null)
{
continue;
}
var text = GetBoxPart(part.Value);
if (text.Length > 1)
{
throw new InvalidOperationException("A box part cannot contain more than one character.");
}
lookup.Add(part, GetBoxPart(part));
lookup.Add(part.Value, GetBoxPart(part.Value));
}
return lookup;

View File

@@ -19,5 +19,10 @@ namespace Spectre.Console
/// An old school ASCII border.
/// </summary>
Ascii = 2,
/// <summary>
/// A rounded border.
/// </summary>
Rounded = 3,
}
}

View File

@@ -73,7 +73,7 @@ namespace Spectre.Console.Composition
/// <summary>
/// The right part of a cell.
/// </summary>
ColumnRight,
CellRight,
/// <summary>
/// The bottom left part of a footer.

View File

@@ -25,7 +25,7 @@ namespace Spectre.Console.Composition
BorderPart.HeaderBottomRight => "|",
BorderPart.CellLeft => "|",
BorderPart.CellSeparator => "|",
BorderPart.ColumnRight => "|",
BorderPart.CellRight => "|",
BorderPart.FooterBottomLeft => "+",
BorderPart.FooterBottom => "-",
BorderPart.FooterBottomSeparator => "-",

View File

@@ -1,7 +1,11 @@
namespace Spectre.Console.Composition
{
internal sealed class NoBorder : Border
/// <summary>
/// Represents an invisible border.
/// </summary>
public sealed class NoBorder : Border
{
/// <inheritdoc/>
protected override string GetBoxPart(BorderPart part)
{
return " ";

View File

@@ -0,0 +1,37 @@
using System;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a rounded border.
/// </summary>
public sealed class RoundedBorder : Border
{
/// <inheritdoc/>
protected override string GetBoxPart(BorderPart part)
{
return part switch
{
BorderPart.HeaderTopLeft => "╭",
BorderPart.HeaderTop => "─",
BorderPart.HeaderTopSeparator => "┬",
BorderPart.HeaderTopRight => "╮",
BorderPart.HeaderLeft => "│",
BorderPart.HeaderSeparator => "│",
BorderPart.HeaderRight => "│",
BorderPart.HeaderBottomLeft => "├",
BorderPart.HeaderBottom => "─",
BorderPart.HeaderBottomSeparator => "┼",
BorderPart.HeaderBottomRight => "┤",
BorderPart.CellLeft => "│",
BorderPart.CellSeparator => "│",
BorderPart.CellRight => "│",
BorderPart.FooterBottomLeft => "╰",
BorderPart.FooterBottom => "─",
BorderPart.FooterBottomSeparator => "┴",
BorderPart.FooterBottomRight => "╯",
_ => throw new InvalidOperationException("Unknown box part."),
};
}
}
}

View File

@@ -25,7 +25,7 @@ namespace Spectre.Console.Composition
BorderPart.HeaderBottomRight => "┤",
BorderPart.CellLeft => "│",
BorderPart.CellSeparator => "│",
BorderPart.ColumnRight => "│",
BorderPart.CellRight => "│",
BorderPart.FooterBottomLeft => "└",
BorderPart.FooterBottom => "─",
BorderPart.FooterBottomSeparator => "┴",

View File

@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
@@ -17,41 +18,102 @@ namespace Spectre.Console
/// </summary>
public Grid()
{
_table = new Table(BorderKind.None, showHeaders: false);
_table = new Table
{
Border = BorderKind.None,
ShowHeaders = false,
IsGrid = true,
PadRightCell = false,
};
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
public Measurement Measure(RenderContext context, int maxWidth)
{
return _table.Measure(encoding, maxWidth);
return ((IRenderable)_table).Measure(context, maxWidth);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
public IEnumerable<Segment> Render(RenderContext context, int width)
{
return _table.Render(encoding, width);
return ((IRenderable)_table).Render(context, width);
}
/// <summary>
/// Adds a single column to the grid.
/// Adds a column to the grid.
/// </summary>
public void AddColumn()
{
_table.AddColumn(string.Empty);
AddColumn(new GridColumn());
}
/// <summary>
/// Adds the specified number of columns to the grid.
/// Adds a column to the grid.
/// </summary>
/// <param name="count">The number of columns.</param>
/// <param name="column">The column to add.</param>
public void AddColumn(GridColumn column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (_table.RowCount > 0)
{
throw new InvalidOperationException("Cannot add new columns to grid with existing rows.");
}
// Only pad the most right cell if we've explicitly set a padding.
_table.PadRightCell = column.Padding != null;
_table.AddColumn(new TableColumn(string.Empty)
{
Width = column.Width,
NoWrap = column.NoWrap,
Padding = column.Padding ?? new Padding(0, 2),
Alignment = column.Alignment,
});
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="count">The number of columns to add.</param>
public void AddColumns(int count)
{
for (var i = 0; i < count; i++)
for (var index = 0; index < count; index++)
{
_table.AddColumn(string.Empty);
AddColumn(new GridColumn());
}
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params GridColumn[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary>
/// Adds an empty row to the grid.
/// </summary>
public void AddEmptyRow()
{
var columns = new string[_table.ColumnCount];
Enumerable.Range(0, _table.ColumnCount).ForEach(index => columns[index] = string.Empty);
AddRow(columns);
}
/// <summary>
/// Adds a new row to the grid.
/// </summary>

View File

@@ -0,0 +1,30 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a grid column.
/// </summary>
public sealed class GridColumn
{
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap { get; set; }
/// <summary>
/// Gets or sets the padding of the column.
/// </summary>
public Padding? Padding { get; set; }
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Text;
namespace Spectre.Console.Composition
{
@@ -11,17 +10,17 @@ namespace Spectre.Console.Composition
/// <summary>
/// Measures the renderable object.
/// </summary>
/// <param name="encoding">The encoding to use.</param>
/// <param name="context">The render context.</param>
/// <param name="maxWidth">The maximum allowed width.</param>
/// <returns>The width of the object.</returns>
int Measure(Encoding encoding, int maxWidth);
/// <returns>The minimum and maximum width of the object.</returns>
Measurement Measure(RenderContext context, int maxWidth);
/// <summary>
/// Renders the object.
/// </summary>
/// <param name="encoding">The encoding to use.</param>
/// <param name="width">The width of the render area.</param>
/// <param name="context">The render context.</param>
/// <param name="maxWidth">The maximum allowed width.</param>
/// <returns>A collection of segments.</returns>
IEnumerable<Segment> Render(Encoding encoding, int width);
IEnumerable<Segment> Render(RenderContext context, int maxWidth);
}
}

View File

@@ -16,7 +16,7 @@ namespace Spectre.Console
Right = 1,
/// <summary>
/// Centered
/// Centered.
/// </summary>
Center = 2,
}

View File

@@ -0,0 +1,77 @@
using System;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a measurement.
/// </summary>
public struct Measurement : IEquatable<Measurement>
{
/// <summary>
/// Gets the minimum width.
/// </summary>
public int Min { get; }
/// <summary>
/// Gets the maximum width.
/// </summary>
public int Max { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Measurement"/> struct.
/// </summary>
/// <param name="min">The minimum width.</param>
/// <param name="max">The maximum width.</param>
public Measurement(int min, int max)
{
Min = min;
Max = max;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Measurement measurement && Equals(measurement);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
var hash = (int)2166136261;
hash = (hash * 16777619) ^ Min.GetHashCode();
hash = (hash * 16777619) ^ Max.GetHashCode();
return hash;
}
}
/// <inheritdoc/>
public bool Equals(Measurement other)
{
return Min == other.Min && Max == other.Max;
}
/// <summary>
/// Checks if two <see cref="Measurement"/> instances are equal.
/// </summary>
/// <param name="left">The first measurement instance to compare.</param>
/// <param name="right">The second measurement instance to compare.</param>
/// <returns><c>true</c> if the two measurements are equal, otherwise <c>false</c>.</returns>
public static bool operator ==(Measurement left, Measurement right)
{
return left.Equals(right);
}
/// <summary>
/// Checks if two <see cref="Measurement"/> instances are not equal.
/// </summary>
/// <param name="left">The first measurement instance to compare.</param>
/// <param name="right">The second measurement instance to compare.</param>
/// <returns><c>true</c> if the two measurements are not equal, otherwise <c>false</c>.</returns>
public static bool operator !=(Measurement left, Measurement right)
{
return !(left == right);
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a measurement.
/// </summary>
public struct Padding : IEquatable<Padding>
{
/// <summary>
/// Gets the left padding.
/// </summary>
public int Left { get; }
/// <summary>
/// Gets the right padding.
/// </summary>
public int Right { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Padding"/> struct.
/// </summary>
/// <param name="left">The left padding.</param>
/// <param name="right">The right padding.</param>
public Padding(int left, int right)
{
Left = left;
Right = right;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Padding padding && Equals(padding);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
var hash = (int)2166136261;
hash = (hash * 16777619) ^ Left.GetHashCode();
hash = (hash * 16777619) ^ Right.GetHashCode();
return hash;
}
}
/// <inheritdoc/>
public bool Equals(Padding other)
{
return Left == other.Left && Right == other.Right;
}
/// <summary>
/// Checks if two <see cref="Padding"/> instances are equal.
/// </summary>
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
/// <returns><c>true</c> if the two instances are equal, otherwise <c>false</c>.</returns>
public static bool operator ==(Padding left, Padding right)
{
return left.Equals(right);
}
/// <summary>
/// Checks if two <see cref="Padding"/> instances are not equal.
/// </summary>
/// <param name="left">The first <see cref="Padding"/> instance to compare.</param>
/// <param name="right">The second <see cref="Padding"/> instance to compare.</param>
/// <returns><c>true</c> if the two instances are not equal, otherwise <c>false</c>.</returns>
public static bool operator !=(Padding left, Padding right)
{
return !(left == right);
}
/// <summary>
/// Gets the horizontal padding.
/// </summary>
/// <returns>The horizontal padding.</returns>
public int GetHorizontalPadding()
{
return Left + Right;
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Composition;
namespace Spectre.Console
@@ -10,107 +9,122 @@ namespace Spectre.Console
/// </summary>
public sealed class Panel : IRenderable
{
private const int EdgeWidth = 2;
private readonly IRenderable _child;
private readonly bool _fit;
private readonly Justify _content;
/// <summary>
/// Gets or sets a value indicating whether or not to use
/// a "safe" border on legacy consoles that might not be able
/// to render non-ASCII characters. Defaults to <c>true</c>.
/// </summary>
public bool SafeBorder { get; set; } = true;
/// <summary>
/// Gets or sets the kind of border to use.
/// </summary>
public BorderKind Border { get; set; } = BorderKind.Square;
/// <summary>
/// Gets or sets the alignment of the panel contents.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the panel should
/// fit the available space. If <c>false</c>, the panel width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public bool Expand { get; set; }
/// <summary>
/// Gets or sets the padding.
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
/// <param name="child">The child.</param>
/// <param name="fit">Whether or not to fit the panel to it's parent.</param>
/// <param name="content">The justification of the panel content.</param>
public Panel(IRenderable child, bool fit = false, Justify content = Justify.Left)
/// <param name="content">The panel content.</param>
public Panel(IRenderable content)
{
_child = child;
_fit = fit;
_content = content;
_child = content ?? throw new System.ArgumentNullException(nameof(content));
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{
var childWidth = _child.Measure(encoding, maxWidth);
return childWidth + 4;
var childWidth = _child.Measure(context, maxWidth);
return new Measurement(childWidth.Min + 2 + Padding.GetHorizontalPadding(), childWidth.Max + 2 + Padding.GetHorizontalPadding());
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
{
var childWidth = width - 4;
if (!_fit)
var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = width - EdgeWidth - paddingWidth;
if (!Expand)
{
childWidth = _child.Measure(encoding, width - 2);
var measurement = _child.Measure(context, width - EdgeWidth - paddingWidth);
childWidth = measurement.Max;
}
var result = new List<Segment>();
var panelWidth = childWidth + 2;
var panelWidth = childWidth + paddingWidth;
result.Add(new Segment("┌"));
result.Add(new Segment(new string('─', panelWidth)));
result.Add(new Segment("┐"));
result.Add(new Segment("\n"));
// Panel top
var result = new List<Segment>
{
new Segment(border.GetPart(BorderPart.HeaderTopLeft)),
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth)),
new Segment(border.GetPart(BorderPart.HeaderTopRight)),
new Segment("\n"),
};
// Render the child.
var childSegments = _child.Render(encoding, childWidth);
var childContext = context.WithJustification(Alignment);
var childSegments = _child.Render(childContext, childWidth);
// Split the child segments into lines.
var lines = Segment.SplitLines(childSegments, childWidth);
foreach (var line in lines)
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
{
result.Add(new Segment("│ "));
result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
// Left padding
if (Padding.Left > 0)
{
result.Add(new Segment(new string(' ', Padding.Left)));
}
var content = new List<Segment>();
content.AddRange(line);
var length = line.Sum(segment => segment.CellLength(encoding));
// Do we need to pad the panel?
var length = line.Sum(segment => segment.CellLength(context.Encoding));
if (length < childWidth)
{
if (_content == Justify.Right)
{
var diff = childWidth - length;
content.Add(new Segment(new string(' ', diff)));
}
else if (_content == Justify.Center)
{
var diff = (childWidth - length) / 2;
content.Add(new Segment(new string(' ', diff)));
}
}
foreach (var segment in line)
{
content.Add(segment.StripLineEndings());
}
if (length < childWidth)
{
if (_content == Justify.Left)
{
var diff = childWidth - length;
content.Add(new Segment(new string(' ', diff)));
}
else if (_content == Justify.Center)
{
var diff = (childWidth - length) / 2;
content.Add(new Segment(new string(' ', diff)));
var remainder = (childWidth - length) % 2;
if (remainder != 0)
{
content.Add(new Segment(new string(' ', remainder)));
}
}
var diff = childWidth - length;
content.Add(new Segment(new string(' ', diff)));
}
result.AddRange(content);
result.Add(new Segment(" │"));
// Right padding
if (Padding.Right > 0)
{
result.Add(new Segment(new string(' ', Padding.Right)));
}
result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
result.Add(new Segment("\n"));
}
result.Add(new Segment("└"));
result.Add(new Segment(new string('─', panelWidth)));
result.Add(new Segment("┘"));
// Panel bottom
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
result.Add(new Segment("\n"));
return result;

View File

@@ -0,0 +1,54 @@
using System.Text;
namespace Spectre.Console.Composition
{
/// <summary>
/// Represents a render context.
/// </summary>
public sealed class RenderContext
{
/// <summary>
/// Gets the console's output encoding.
/// </summary>
public Encoding Encoding { get; }
/// <summary>
/// Gets a value indicating whether or not this a legacy console (i.e. cmd.exe).
/// </summary>
public bool LegacyConsole { get; }
/// <summary>
/// Gets a value indicating whether or not unicode is supported.
/// </summary>
public bool Unicode { get; }
/// <summary>
/// Gets the current justification.
/// </summary>
public Justify? Justification { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RenderContext"/> class.
/// </summary>
/// <param name="encoding">The console's output encoding.</param>
/// <param name="legacyConsole">A value indicating whether or not this a legacy console (i.e. cmd.exe).</param>
/// <param name="justification">The justification to use when rendering.</param>
public RenderContext(Encoding encoding, bool legacyConsole, Justify? justification = null)
{
Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding));
LegacyConsole = legacyConsole;
Justification = justification;
Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode;
}
/// <summary>
/// Creates a new context with the specified justification.
/// </summary>
/// <param name="justification">The justification.</param>
/// <returns>A new <see cref="RenderContext"/> instance with the specified justification.</returns>
public RenderContext WithJustification(Justify? justification)
{
return new RenderContext(Encoding, LegacyConsole, justification);
}
}
}

View File

@@ -50,7 +50,12 @@ namespace Spectre.Console.Composition
private Segment(string text, Style style, bool lineBreak)
{
Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
Text = text.NormalizeLineEndings();
Style = style;
IsLineBreak = lineBreak;
}
@@ -89,7 +94,7 @@ namespace Spectre.Console.Composition
/// </summary>
/// <param name="offset">The offset where to split the segment.</param>
/// <returns>One or two new segments representing the split.</returns>
public (Segment First, Segment Second) Split(int offset)
public (Segment First, Segment? Second) Split(int offset)
{
if (offset < 0)
{
@@ -211,5 +216,21 @@ namespace Spectre.Console.Composition
return lines;
}
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
{
foreach (var cell in cells)
{
if (cell.Count < cellHeight)
{
while (cell.Count != cellHeight)
{
cell.Add(new SegmentLine());
}
}
}
return cells;
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a table.
/// </summary>
public sealed partial class Table
{
private const int EdgeCount = 2;
// Calculate the widths of each column, including padding, not including borders.
// Ported from Rich by Will McGugan, licensed under MIT.
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
private List<int> CalculateColumnWidths(RenderContext options, int maxWidth)
{
var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth));
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
if (tableWidth > maxWidth)
{
var wrappable = _columns.Select(c => !c.NoWrap).ToList();
widths = CollapseWidths(widths, wrappable, maxWidth);
tableWidth = widths.Sum();
// last resort, reduce columns evenly
if (tableWidth > maxWidth)
{
var excessWidth = tableWidth - maxWidth;
widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
tableWidth = widths.Sum();
}
}
if (tableWidth < maxWidth && ShouldExpand())
{
var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
}
return widths;
}
// Reduce widths so that the total is less or equal to the max width.
// Ported from Rich by Will McGugan, licensed under MIT.
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442
private static List<int> CollapseWidths(List<int> widths, List<bool> wrappable, int maxWidth)
{
var totalWidth = widths.Sum();
var excessWidth = totalWidth - maxWidth;
if (wrappable.AnyTrue())
{
while (totalWidth != 0 && excessWidth > 0)
{
var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
.Where(x => x.allowWrap)
.Max(x => x.width);
var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
var columnDifference = maxColumn - secondMaxColumn;
var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
if (!ratios.Any(x => x != 0) || columnDifference == 0)
{
break;
}
var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList();
widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths);
totalWidth = widths.Sum();
excessWidth = totalWidth - maxWidth;
}
}
return widths;
}
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{
var padding = column.Padding.GetHorizontalPadding();
// Predetermined width?
if (column.Width != null)
{
return (column.Width.Value + padding, column.Width.Value + padding);
}
var columnIndex = _columns.IndexOf(column);
var rows = _rows.Select(row => row[columnIndex]);
var minWidths = new List<int>();
var maxWidths = new List<int>();
// Include columns in measurement
var measure = ((IRenderable)column.Text).Measure(options, maxWidth);
minWidths.Add(measure.Min);
maxWidths.Add(measure.Max);
foreach (var row in rows)
{
measure = ((IRenderable)row).Measure(options, maxWidth);
minWidths.Add(measure.Min);
maxWidths.Add(measure.Max);
}
return (minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
}
private int GetExtraWidth(bool includePadding)
{
var separators = _columns.Count - 1;
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
return separators + EdgeCount + padding;
}
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
@@ -10,31 +9,65 @@ namespace Spectre.Console
/// <summary>
/// Represents a table.
/// </summary>
public sealed class Table : IRenderable
public sealed partial class Table : IRenderable
{
private readonly List<Text> _columns;
private readonly List<TableColumn> _columns;
private readonly List<List<Text>> _rows;
private readonly Border _border;
private readonly BorderKind _borderKind;
private readonly bool _showHeaders;
/// <summary>
/// Gets the number of columns in the table.
/// </summary>
public int ColumnCount => _columns.Count;
/// <summary>
/// Gets the number of rows in the table.
/// </summary>
public int RowCount => _rows.Count;
/// <summary>
/// Gets or sets the kind of border to use.
/// </summary>
public BorderKind Border { get; set; } = BorderKind.Square;
/// <summary>
/// Gets or sets a value indicating whether or not table headers should be shown.
/// </summary>
public bool ShowHeaders { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the table should
/// fit the available space. If <c>false</c>, the table width will be
/// auto calculated. Defaults to <c>false</c>.
/// </summary>
public bool Expand { get; set; }
/// <summary>
/// Gets or sets the width of the table.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not to use
/// a "safe" border on legacy consoles that might not be able
/// to render non-ASCII characters. Defaults to <c>true</c>.
/// </summary>
public bool SafeBorder { get; set; } = true;
// Whether this is a grid or not.
internal bool IsGrid { get; set; }
// Whether or not the most right cell should be padded.
// This is almost always the case, unless we're rendering
// a grid without explicit padding in the last cell.
internal bool PadRightCell { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="Table"/> class.
/// </summary>
/// <param name="border">The border to use.</param>
/// <param name="showHeaders">Whether or not to show table headers.</param>
public Table(BorderKind border = BorderKind.Square, bool showHeaders = true)
public Table()
{
_columns = new List<Text>();
_columns = new List<TableColumn>();
_rows = new List<List<Text>>();
_border = Border.GetBorder(border);
_borderKind = border;
_showHeaders = showHeaders;
}
/// <summary>
@@ -48,7 +81,26 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column));
}
_columns.Add(Text.New(column));
AddColumn(new TableColumn(column));
}
/// <summary>
/// Adds a column to the table.
/// </summary>
/// <param name="column">The column to add.</param>
public void AddColumn(TableColumn column)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (_rows.Count > 0)
{
throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
}
_columns.Add(column);
}
/// <summary>
@@ -62,7 +114,37 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
_columns.AddRange(columns.Select(column => Text.New(column)));
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary>
/// Adds multiple columns to the table.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params TableColumn[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary>
/// Adds an empty row to the table.
/// </summary>
public void AddEmptyRow()
{
var columns = new string[ColumnCount];
Enumerable.Range(0, ColumnCount).ForEach(index => columns[index] = string.Empty);
AddRow(columns);
}
/// <summary>
@@ -90,66 +172,66 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{
// Calculate the max width for each column
var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
if (context is null)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
throw new ArgumentNullException(nameof(context));
}
// We now know the max width of each column, so let's recalculate the width
return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
if (Width != null)
{
maxWidth = Math.Min(Width.Value, maxWidth);
}
maxWidth -= GetExtraWidth(includePadding: true);
var measurements = _columns.Select(column => MeasureColumn(column, context, maxWidth)).ToList();
var min = measurements.Sum(x => x.Min) + GetExtraWidth(includePadding: true);
var max = Width ?? measurements.Sum(x => x.Max) + GetExtraWidth(includePadding: true);
return new Measurement(min, max);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
{
var showBorder = _borderKind != BorderKind.None;
var hideBorder = _borderKind == BorderKind.None;
var leftRightBorderWidth = _borderKind == BorderKind.None ? 0 : 2;
var columnPadding = _borderKind == BorderKind.None ? _columns.Count : _columns.Count * 2;
var separatorCount = _borderKind == BorderKind.None ? 0 : _columns.Count - 1;
// Calculate the max width for each column.
var maxColumnWidth = (width - (leftRightBorderWidth + columnPadding + separatorCount)) / _columns.Count;
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
if (context is null)
{
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
{
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
if (columnWidth > columnWidths[columnIndex])
{
columnWidths[columnIndex] = columnWidth;
}
}
throw new ArgumentNullException(nameof(context));
}
// We now know the max width of each column, so let's recalculate the width
width = columnWidths.Sum() + leftRightBorderWidth + columnPadding + separatorCount;
var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var showBorder = Border != BorderKind.None;
var hideBorder = Border == BorderKind.None;
var hasRows = _rows.Count > 0;
var maxWidth = width;
if (Width != null)
{
maxWidth = Math.Min(Width.Value, maxWidth);
}
maxWidth -= GetExtraWidth(includePadding: true);
// Calculate the column and table widths
var columnWidths = CalculateColumnWidths(context, maxWidth);
// Update the table width.
width = columnWidths.Sum() + GetExtraWidth(includePadding: true);
var rows = new List<List<Text>>();
if (_showHeaders)
if (ShowHeaders)
{
// Add columns to top of rows
rows.Add(new List<Text>(_columns));
rows.Add(new List<Text>(_columns.Select(c => c.Text)));
}
// Add tows.
// Add rows.
rows.AddRange(_rows);
// Iterate all rows.
// Iterate all rows
var result = new List<Segment>();
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
{
@@ -157,9 +239,12 @@ namespace Spectre.Console
// Get the list of cells for the row and calculate the cell height
var cells = new List<List<SegmentLine>>();
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s)))
foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
{
var lines = Segment.SplitLines(cell.Render(encoding, rowWidth));
var justification = _columns[columnIndex].Alignment;
var childContext = context.WithJustification(justification);
var lines = Segment.SplitLines(((IRenderable)cell).Render(childContext, rowWidth));
cellHeight = Math.Max(cellHeight, lines.Count);
cells.Add(lines);
}
@@ -167,20 +252,22 @@ namespace Spectre.Console
// Show top of header?
if (firstRow && showBorder)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft)));
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding
var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, padding.Right))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight)));
result.Add(Segment.LineBreak());
}
@@ -188,91 +275,102 @@ namespace Spectre.Console
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
{
// Make cells the same shape
MakeSameHeight(cellHeight, cells);
cells = Segment.MakeSameHeight(cellHeight, cells);
var w00t = cells.Enumerate().ToArray();
foreach (var (cellIndex, firstCell, lastCell, cell) in w00t)
foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate())
{
if (firstCell && showBorder)
{
// Show left column edge
result.Add(new Segment(_border.GetPart(BorderPart.CellLeft)));
result.Add(new Segment(border.GetPart(BorderPart.CellLeft)));
}
// Pad column on left side.
if (showBorder)
if (showBorder || IsGrid)
{
result.Add(new Segment(" "));
var leftPadding = _columns[cellIndex].Padding.Left;
if (leftPadding > 0)
{
result.Add(new Segment(new string(' ', leftPadding)));
}
}
// Add content
result.AddRange(cell[cellRowIndex]);
// Pad cell content right
var length = cell[cellRowIndex].Sum(segment => segment.CellLength(encoding));
var length = cell[cellRowIndex].Sum(segment => segment.CellLength(context.Encoding));
if (length < columnWidths[cellIndex])
{
result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
}
// Pad column on the right side
if (showBorder || (hideBorder && !lastCell))
if (showBorder || (hideBorder && !lastCell) || (hideBorder && lastCell && IsGrid && PadRightCell))
{
result.Add(new Segment(" "));
var rightPadding = _columns[cellIndex].Padding.Right;
if (rightPadding > 0)
{
result.Add(new Segment(new string(' ', rightPadding)));
}
}
if (lastCell && showBorder)
{
// Add right column edge
result.Add(new Segment(_border.GetPart(BorderPart.ColumnRight)));
result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
}
else if (showBorder || (hideBorder && !lastCell))
else if (showBorder)
{
// Add column separator
result.Add(new Segment(_border.GetPart(BorderPart.CellSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.CellSeparator)));
}
}
result.Add(Segment.LineBreak());
}
// Show bottom of header?
if (firstRow && showBorder)
// Show header separator?
if (firstRow && showBorder && ShowHeaders && hasRows)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding
var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottom, padding.Right))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight)));
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight)));
result.Add(Segment.LineBreak());
}
// Show bottom of footer?
if (lastRow && showBorder)
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
var padding = _columns[columnIndex].Padding;
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Left))); // Left padding
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, columnWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, padding.Right))); // Right padding
if (!lastColumn)
{
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomSeparator)));
}
}
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
result.Add(Segment.LineBreak());
}
}
@@ -280,18 +378,9 @@ namespace Spectre.Console
return result;
}
private static void MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
private bool ShouldExpand()
{
foreach (var cell in cells)
{
if (cell.Count < cellHeight)
{
while (cell.Count != cellHeight)
{
cell.Add(new SegmentLine());
}
}
}
return Expand || Width != null;
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a table column.
/// </summary>
public sealed class TableColumn
{
/// <summary>
/// Gets the text associated with the column.
/// </summary>
public Text Text { get; }
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the padding of the column.
/// </summary>
public Padding Padding { get; set; }
/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
/// </summary>
public bool NoWrap { get; set; }
/// <summary>
/// Gets or sets the alignment of the column.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TableColumn"/> class.
/// </summary>
/// <param name="text">The table column text.</param>
public TableColumn(string text)
{
Text = Text.New(text ?? throw new ArgumentNullException(nameof(text)));
Width = null;
Padding = new Padding(1, 1);
NoWrap = false;
Alignment = null;
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
@@ -19,6 +18,11 @@ namespace Spectre.Console
private readonly List<Span> _spans;
private string _text;
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify Alignment { get; set; } = Justify.Left;
private sealed class Span
{
public int Start { get; }
@@ -39,7 +43,7 @@ namespace Spectre.Console
/// <param name="text">The text.</param>
internal Text(string text)
{
_text = text ?? throw new ArgumentNullException(nameof(text));
_text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
_spans = new List<Span>();
}
@@ -52,12 +56,26 @@ namespace Spectre.Console
/// <param name="decoration">The text decoration.</param>
/// <returns>A <see cref="Text"/> instance.</returns>
public static Text New(
string text, Color? foreground = null, Color? background = null, Decoration? decoration = null)
string text,
Color? foreground = null,
Color? background = null,
Decoration? decoration = null)
{
var result = MarkupParser.Parse(text, new Style(foreground, background, decoration));
return result;
}
/// <summary>
/// Sets the text alignment.
/// </summary>
/// <param name="alignment">The text alignment.</param>
/// <returns>The same <see cref="Text"/> instance.</returns>
public Text WithAlignment(Justify alignment)
{
Alignment = alignment;
return this;
}
/// <summary>
/// Appends some text with the specified color and decorations.
/// </summary>
@@ -98,21 +116,32 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public int Measure(Encoding encoding, int maxWidth)
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{
var lines = Segment.SplitLines(Render(encoding, maxWidth));
if (lines.Count == 0)
if (string.IsNullOrEmpty(_text))
{
return 0;
return new Measurement(1, 1);
}
return lines.Max(x => x.Length);
// TODO: Write some kind of tokenizer for this
var min = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth))
.SelectMany(line => line.Select(segment => segment.Text.Length))
.Max();
var max = _text.SplitLines().Max(x => Cell.GetCellLength(context.Encoding, x));
return new Measurement(min, max);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(Encoding encoding, int width)
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
{
if (string.IsNullOrWhiteSpace(_text))
if (string.IsNullOrEmpty(_text))
{
return Array.Empty<Segment>();
}
if (width == 0)
{
return Array.Empty<Segment>();
}
@@ -120,14 +149,50 @@ namespace Spectre.Console
var result = new List<Segment>();
var segments = SplitLineBreaks(CreateSegments());
var justification = context.Justification ?? Alignment;
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
{
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
if (length < width)
{
// Justify right side
if (justification == Justify.Right)
{
var diff = width - length;
result.Add(new Segment(new string(' ', diff)));
}
else if (justification == Justify.Center)
{
var diff = (width - length) / 2;
result.Add(new Segment(new string(' ', diff)));
}
}
// Render the line.
foreach (var segment in line)
{
result.Add(segment.StripLineEndings());
}
if (!last)
// Justify left side
if (length < width)
{
if (justification == Justify.Center)
{
var diff = (width - length) / 2;
result.Add(new Segment(new string(' ', diff)));
var remainder = (width - length) % 2;
if (remainder != 0)
{
result.Add(new Segment(new string(' ', remainder)));
}
}
}
if (!last || line.Count == 0)
{
result.Add(Segment.LineBreak());
}
@@ -136,7 +201,7 @@ namespace Spectre.Console
return result;
}
private IEnumerable<Segment> SplitLineBreaks(IEnumerable<Segment> segments)
private static IEnumerable<Segment> SplitLineBreaks(IEnumerable<Segment> segments)
{
// Creates individual segments of line breaks.
var result = new List<Segment>();
@@ -149,7 +214,10 @@ namespace Spectre.Console
var index = segment.Text.IndexOf("\n", StringComparison.OrdinalIgnoreCase);
if (index == -1)
{
result.Add(segment);
if (!string.IsNullOrEmpty(segment.Text))
{
result.Add(segment);
}
}
else
{
@@ -160,7 +228,11 @@ namespace Spectre.Console
}
result.Add(Segment.LineBreak());
queue.Push(new Segment(second.Text.Substring(1), second.Style));
if (second != null)
{
queue.Push(new Segment(second.Text.Substring(1), second.Style));
}
}
}

View File

@@ -26,7 +26,9 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(renderable));
}
foreach (var segment in renderable.Render(console.Encoding, console.Width))
var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
foreach (var segment in renderable.Render(options, console.Width))
{
if (!segment.Style.Equals(Style.Plain))
{

View File

@@ -60,12 +60,19 @@ namespace Spectre.Console.Internal
return;
}
_out.Write(AnsiBuilder.GetAnsi(
_system,
text.NormalizeLineEndings(native: true),
Decoration,
Foreground,
Background));
var parts = text.NormalizeLineEndings().Split(new[] { '\n' });
foreach (var (_, _, last, part) in parts.Enumerate())
{
if (!string.IsNullOrEmpty(part))
{
_out.Write(AnsiBuilder.GetAnsi(_system, part, Decoration, Foreground, Background));
}
if (!last)
{
_out.Write(Environment.NewLine);
}
}
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Spectre.Console.Internal
{
if (supportsAnsi)
{
var regex = new Regex("^Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)$");
var regex = new Regex("^Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)\\s*$");
var match = regex.Match(RuntimeInformation.OSDescription);
if (match.Success && int.TryParse(match.Groups["major"].Value, out var major))
{

View File

@@ -45,7 +45,7 @@ namespace Spectre.Console.Internal
return ColorPalette.EightBit[number];
}
public static string GetName(int number)
public static string? GetName(int number)
{
_nameLookup.TryGetValue(number, out var name);
return name;

View File

@@ -15,14 +15,18 @@ namespace Spectre.Console.Internal
{
{ "none", Decoration.None },
{ "bold", Decoration.Bold },
{ "b", Decoration.Bold },
{ "dim", Decoration.Dim },
{ "italic", Decoration.Italic },
{ "i", Decoration.Italic },
{ "underline", Decoration.Underline },
{ "u", Decoration.Underline },
{ "invert", Decoration.Invert },
{ "conceal", Decoration.Conceal },
{ "slowblink", Decoration.SlowBlink },
{ "rapidblink", Decoration.RapidBlink },
{ "strikethrough", Decoration.Strikethrough },
{ "s", Decoration.Strikethrough },
};
}

View File

@@ -6,6 +6,19 @@ namespace Spectre.Console.Internal
{
internal static class EnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
{
action(item);
}
}
public static bool AnyTrue(this IEnumerable<bool> source)
{
return source.Any(b => b);
}
public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate<T>(this IEnumerable<T> source)
{
if (source is null)
@@ -40,5 +53,18 @@ namespace Spectre.Console.Internal
{
return source.Select((value, index) => func(value, index));
}
public static IEnumerable<(TFirst First, TSecond Second)> Zip<TFirst, TSecond>(
this IEnumerable<TFirst> source, IEnumerable<TSecond> first)
{
return source.Zip(first, (first, second) => (first, second));
}
public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip<TFirst, TSecond, TThird>(
this IEnumerable<TFirst> first, IEnumerable<TSecond> second, IEnumerable<TThird> third)
{
return first.Zip(second, (a, b) => (a, b))
.Zip(third, (a, b) => (a.a, a.b, b));
}
}
}

View File

@@ -17,9 +17,9 @@ namespace Spectre.Console.Internal
public static string NormalizeLineEndings(this string text, bool native = false)
{
var normalized = text?.Replace("\r\n", "\n")
?.Replace("\r", string.Empty);
text ??= string.Empty;
var normalized = text?.Replace("\r\n", "\n")?.Replace("\r", string.Empty) ?? string.Empty;
if (native && !_alreadyNormalized)
{
normalized = normalized.Replace("\n", Environment.NewLine);
@@ -30,7 +30,8 @@ namespace Spectre.Console.Internal
public static string[] SplitLines(this string text)
{
return text.NormalizeLineEndings().Split(new[] { '\n' }, StringSplitOptions.None);
var result = text?.NormalizeLineEndings()?.Split(new[] { '\n' }, StringSplitOptions.None);
return result ?? Array.Empty<string>();
}
}
}

View File

@@ -6,7 +6,7 @@ namespace Spectre.Console.Internal
{
internal static class MarkupParser
{
public static Text Parse(string text, Style style = null)
public static Text Parse(string text, Style? style = null)
{
style ??= Style.Plain;
@@ -18,6 +18,10 @@ namespace Spectre.Console.Internal
while (tokenizer.MoveNext())
{
var token = tokenizer.Current;
if (token == null)
{
break;
}
if (token.Kind == MarkupTokenKind.Open)
{

View File

@@ -7,7 +7,7 @@ namespace Spectre.Console.Internal
{
private readonly StringBuffer _reader;
public MarkupToken Current { get; private set; }
public MarkupToken? Current { get; private set; }
public MarkupTokenizer(string text)
{

View File

@@ -1,10 +1,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Spectre.Console.Internal
{
internal sealed class StringBuffer : IDisposable
{
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "False positive")]
private readonly StringReader _reader;
private readonly int _length;

View File

@@ -1,4 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Spectre.Console.Internal
{
@@ -12,16 +14,23 @@ namespace Spectre.Console.Internal
throw new InvalidOperationException(error);
}
if (style == null)
{
// This should not happen, but we need to please the compiler
// which cannot know that style isn't null here.
throw new InvalidOperationException("Could not parse style.");
}
return style;
}
public static bool TryParse(string text, out Style style)
public static bool TryParse(string text, out Style? style)
{
style = Parse(text, out var error);
return error == null;
}
private static Style Parse(string text, out string error)
private static Style? Parse(string text, out string? error)
{
var effectiveDecoration = (Decoration?)null;
var effectiveForeground = (Color?)null;
@@ -57,16 +66,30 @@ namespace Spectre.Console.Internal
var color = ColorTable.GetColor(part);
if (color == null)
{
if (!foreground)
if (part.StartsWith("#", StringComparison.OrdinalIgnoreCase))
{
error = $"Could not find color '{part}'.";
color = ParseHexColor(part, out error);
if (!string.IsNullOrWhiteSpace(error))
{
return null;
}
}
else if (part.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
{
color = ParseRgbColor(part, out error);
if (!string.IsNullOrWhiteSpace(error))
{
return null;
}
}
else
{
error = $"Could not find color or style '{part}'.";
}
error = !foreground
? $"Could not find color '{part}'."
: $"Could not find color or style '{part}'.";
return null;
return null;
}
}
if (foreground)
@@ -95,5 +118,82 @@ namespace Spectre.Console.Internal
error = null;
return new Style(effectiveForeground, effectiveBackground, effectiveDecoration);
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private static Color? ParseHexColor(string hex, out string? error)
{
error = null;
hex ??= string.Empty;
hex = hex.Replace("#", string.Empty).Trim();
try
{
if (!string.IsNullOrWhiteSpace(hex))
{
if (hex.Length == 6)
{
return new Color(
(byte)Convert.ToUInt32(hex.Substring(0, 2), 16),
(byte)Convert.ToUInt32(hex.Substring(2, 2), 16),
(byte)Convert.ToUInt32(hex.Substring(4, 2), 16));
}
else if (hex.Length == 3)
{
return new Color(
(byte)Convert.ToUInt32(new string(hex[0], 2), 16),
(byte)Convert.ToUInt32(new string(hex[1], 2), 16),
(byte)Convert.ToUInt32(new string(hex[2], 2), 16));
}
}
}
catch (Exception ex)
{
error = $"Invalid hex color '#{hex}'. {ex.Message}";
return null;
}
error = $"Invalid hex color '#{hex}'.";
return null;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private static Color? ParseRgbColor(string rgb, out string? error)
{
try
{
error = null;
var normalized = rgb ?? string.Empty;
if (normalized.Length >= 3)
{
// Trim parenthesises
normalized = normalized.Substring(3).Trim();
if (normalized.StartsWith("(", StringComparison.OrdinalIgnoreCase) &&
normalized.EndsWith(")", StringComparison.OrdinalIgnoreCase))
{
normalized = normalized.Trim('(').Trim(')');
var parts = normalized.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
{
return new Color(
(byte)Convert.ToInt32(parts[0], CultureInfo.InvariantCulture),
(byte)Convert.ToInt32(parts[1], CultureInfo.InvariantCulture),
(byte)Convert.ToInt32(parts[2], CultureInfo.InvariantCulture));
}
}
}
}
catch (Exception ex)
{
error = $"Invalid RGB color '{rgb}'. {ex.Message}";
return null;
}
error = $"Invalid RGB color '{rgb}'.";
return null;
}
}
}

View File

@@ -0,0 +1,75 @@
// Ported from Rich by Will McGugan, licensed under MIT.
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/_ratio.py
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Spectre.Console.Internal
{
internal static class Ratio
{
public static List<int> Reduce(int total, List<int> ratios, List<int> maximums, List<int> values)
{
ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList();
var totalRatio = ratios.Sum();
if (totalRatio <= 0)
{
return values;
}
var totalRemaining = total;
var result = new List<int>();
foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values))
{
if (ratio != 0 && totalRatio > 0)
{
var distributed = (int)Math.Min(maximum, Math.Round((double)(ratio * totalRemaining / totalRatio)));
result.Add(value - distributed);
totalRemaining -= distributed;
totalRatio -= ratio;
}
else
{
result.Add(value);
}
}
return result;
}
public static List<int> Distribute(int total, List<int> ratios, List<int>? minimums = null)
{
if (minimums != null)
{
ratios = ratios.Zip(minimums, (a, b) => (ratio: a, min: b)).Select(a => a.min > 0 ? a.ratio : 0).ToList();
}
var totalRatio = ratios.Sum();
Debug.Assert(totalRatio > 0, "Sum or ratios must be > 0");
var totalRemaining = total;
var distributedTotal = new List<int>();
if (minimums == null)
{
minimums = ratios.Select(_ => 0).ToList();
}
foreach (var (ratio, minimum) in ratios.Zip(minimums, (a, b) => (a, b)))
{
var distributed = (totalRatio > 0)
? Math.Max(minimum, (int)Math.Ceiling(ratio * totalRemaining / (double)totalRatio))
: totalRemaining;
distributedTotal.Add(distributed);
totalRatio -= ratio;
totalRemaining -= distributed;
}
return distributedTotal;
}
}
}

View File

@@ -2,30 +2,29 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<Compile Update="**/ColorPalette.*.cs">
<DependentUpon>**/ColorPalette.cs</DependentUpon>
</Compile>
<Compile Update="Color.*.cs">
<DependentUpon>Color.cs</DependentUpon>
</Compile>
<Compile Update="AnsiConsole.*.cs">
<DependentUpon>AnsiConsole.cs</DependentUpon>
</Compile>
<Compile Update="ConsoleExtensions.*.cs">
<DependentUpon>ConsoleExtensions.cs</DependentUpon>
</Compile>
<None Include="../../gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.4" />
</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.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion>
<GenerateNullableAttributes>False</GenerateNullableAttributes>
</PropertyGroup>
</Project>

View File

@@ -67,7 +67,7 @@ namespace Spectre.Console
/// if the conversion succeeded, or <c>null</c> if the conversion failed.
/// </param>
/// <returns><c>true</c> if s was converted successfully; otherwise, <c>false</c>.</returns>
public static bool TryParse(string text, out Style result)
public static bool TryParse(string text, out Style? result)
{
return StyleParser.TryParse(text, out result);
}
@@ -113,13 +113,13 @@ namespace Spectre.Console
}
/// <inheritdoc/>
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return Equals(obj as Style);
}
/// <inheritdoc/>
public bool Equals(Style other)
public bool Equals(Style? other)
{
if (other == null)
{