Compare commits

...

12 Commits

Author SHA1 Message Date
Patrik Svensson
4cfe55cc27 Emit native line breaks 2020-08-16 13:47:57 +02:00
Patrik Svensson
5b33f80213 Fix line ending problem with text 2020-08-16 12:26:51 +02:00
Patrik Svensson
d7bbaf4a85 Add word wrapping for text
Closes #18
2020-08-14 18:19:24 +02:00
Patrik Svensson
0119364728 Add examples of how to use Spectre.Console 2020-08-12 14:59:17 +02:00
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
58 changed files with 1561 additions and 782 deletions

View File

@@ -13,8 +13,9 @@ for Python.
3. [Usage](#usage) 3. [Usage](#usage)
3.1. [Using the static API](#using-the-static-api) 3.1. [Using the static API](#using-the-static-api)
3.2. [Creating a console](#creating-a-console) 3.2. [Creating a console](#creating-a-console)
4. [Available styles](#available-styles) 4. [Running examples](#running-examples)
5. [Predefined colors](#predefined-colors) 5. [Available styles](#available-styles)
6. [Predefined colors](#predefined-colors)
## Features ## Features
@@ -84,6 +85,42 @@ when manually creating a console, remember that the user's terminal
might not be able to use it, so unless you're creating an IAnsiConsole might not be able to use it, so unless you're creating an IAnsiConsole
for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._ for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._
## Running examples
To see Spectre.Console in action, install the
[dotnet-example](https://github.com/patriksvensson/dotnet-example)
global tool.
```
> dotnet tool install -g dotnet-example
```
Now you can list available examples in this repository:
```
> dotnet example
Examples
Colors Demonstrates how to use colors in the console.
Grid Demonstrates how to render grids in a console.
Panel Demonstrates how to render items in panels.
Table Demonstrates how to render tables in a console.
```
And to run an example:
```
> dotnet example table
┌──────────┬──────────┬────────┐
│ Foo │ Bar │ Baz │
├──────────┼──────────┼────────┤
│ Hello │ World! │ │
│ Bounjour │ le │ monde! │
│ Hej │ Världen! │ │
└──────────┴──────────┴────────┘
```
## Available styles ## Available styles
_NOTE: Not all styles are supported in every terminal._ _NOTE: Not all styles are supported in every terminal._

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Description>Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,78 @@
using Spectre.Console;
namespace ColorExample
{
class Program
{
static void Main(string[] args)
{
/////////////////////////////////////////////////////////////////
// 4-BIT
/////////////////////////////////////////////////////////////////
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold underline]4-bit Colors[/]");
AnsiConsole.WriteLine();
for (var i = 0; i < 16; i++)
{
AnsiConsole.Background = Color.FromInt32(i);
AnsiConsole.Write(string.Format(" {0,-9}", AnsiConsole.Background.ToString()));
AnsiConsole.ResetColors();
if ((i + 1) % 8 == 0)
{
AnsiConsole.WriteLine();
}
}
/////////////////////////////////////////////////////////////////
// 8-BIT
/////////////////////////////////////////////////////////////////
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold underline]8-bit Colors[/]");
AnsiConsole.WriteLine();
for (var i = 0; i < 16; i++)
{
for (var j = 0; j < 16; j++)
{
var number = i * 16 + j;
AnsiConsole.Background = Color.FromInt32(number);
AnsiConsole.Write(string.Format(" {0,-4}", number));
AnsiConsole.ResetColors();
if ((number + 1) % 16 == 0)
{
AnsiConsole.WriteLine();
}
}
}
/////////////////////////////////////////////////////////////////
// 24-BIT
/////////////////////////////////////////////////////////////////
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold underline]24-bit Colors[/]");
AnsiConsole.WriteLine();
var index = 0;
for (var i = 0.0005; i < 1; i += 0.0025)
{
index++;
var color = Utilities.HSL2RGB(i, 0.5, 0.5);
AnsiConsole.Background = new Color(color.R, color.G, color.B);
AnsiConsole.Write(" ");
if (index % 50 == 0)
{
AnsiConsole.WriteLine();
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using Spectre.Console;
namespace ColorExample
{
public static class Utilities
{
// Borrowed from https://geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm
public static Color HSL2RGB(double h, double sl, double l)
{
double v;
double r, g, b;
r = l; // default to gray
g = l;
b = l;
v = (l <= 0.5) ? (l * (1.0 + sl)) : (l + sl - l * sl);
if (v > 0)
{
double m;
double sv;
int sextant;
double fract, vsf, mid1, mid2;
m = l + l - v;
sv = (v - m) / v;
h *= 6.0;
sextant = (int)h;
fract = h - sextant;
vsf = v * sv * fract;
mid1 = m + vsf;
mid2 = v - vsf;
switch (sextant)
{
case 0:
r = v;
g = mid1;
b = m;
break;
case 1:
r = mid2;
g = v;
b = m;
break;
case 2:
r = m;
g = v;
b = mid1;
break;
case 3:
r = m;
g = mid2;
b = v;
break;
case 4:
r = mid1;
g = m;
b = v;
break;
case 5:
r = v;
g = m;
b = mid2;
break;
}
}
return new Color(
Convert.ToByte(r * 255.0f),
Convert.ToByte(g * 255.0f),
Convert.ToByte(b * 255.0f));
}
}
}

14
examples/Grid/Grid.csproj Normal file
View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Description>Demonstrates how to render grids in a console.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

26
examples/Grid/Program.cs Normal file
View File

@@ -0,0 +1,26 @@
using System;
using Spectre.Console;
namespace GridExample
{
public sealed class Program
{
static void Main(string[] args)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] <additional arguments>...]][/]");
AnsiConsole.WriteLine();
var grid = new Grid();
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.");
grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] <LEVEL>", "", "Set the [grey]MSBuild[/] verbosity level.");
AnsiConsole.Render(grid);
}
}
}

View File

@@ -3,12 +3,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Description>Demonstrates how to render items in panels.</Description>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" /> <ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

50
examples/Panel/Program.cs Normal file
View File

@@ -0,0 +1,50 @@
using System;
using Spectre.Console;
namespace PanelExample
{
class Program
{
static void Main(string[] args)
{
var content = Text.Markup(
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
"So I put a 📦 in a 📦\n\n" +
"😅");
AnsiConsole.Render(
new Panel(
new Panel(content)
{
Alignment = Justify.Center,
Border = BorderKind.Rounded
}));
// Left adjusted panel with text
AnsiConsole.Render(new Panel(
new Text("Left adjusted\nLeft"))
{
Expand = true,
Alignment = Justify.Left,
});
// Centered ASCII panel with text
AnsiConsole.Render(new Panel(
new Text("Centered\nCenter"))
{
Expand = true,
Alignment = Justify.Center,
Border = BorderKind.Ascii,
});
// Right adjusted, rounded panel with text
AnsiConsole.Render(new Panel(
new Text("Right adjusted\nRight"))
{
Expand = true,
Alignment = Justify.Right,
Border = BorderKind.Rounded,
});
}
}
}

55
examples/Table/Program.cs Normal file
View File

@@ -0,0 +1,55 @@
using System;
using Spectre.Console;
namespace TableExample
{
class Program
{
static void Main(string[] args)
{
// A simple table§
RenderSimpleTable();
// A big table
RenderBigTable();
}
private static void RenderSimpleTable()
{
// Create the table.
var table = new Table();
table.AddColumn(new TableColumn("[u]Foo[/]"));
table.AddColumn(new TableColumn("[u]Bar[/]"));
table.AddColumn(new TableColumn("[u]Baz[/]"));
// Add some rows
table.AddRow("Hello", "[red]World![/]", "");
table.AddRow("[blue]Bounjour[/]", "[white]le[/]", "[red]monde![/]");
table.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", "");
AnsiConsole.Render(table);
}
private static void RenderBigTable()
{
// Create the table.
var table = new Table { Border = BorderKind.Rounded };
table.AddColumn("[red underline]Foo[/]");
table.AddColumn(new TableColumn("[blue]Bar[/]") { Alignment = Justify.Right, NoWrap = true });
// Add some rows
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
table.AddRow("[yellow]Patrik [green]\"Hello World\"[/] Svensson[/]", "Was [underline]here[/]!");
table.AddEmptyRow();
table.AddRow(
"Lorem ipsum dolor sit amet, consectetur 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.AddEmptyRow();
table.AddRow("Hej 👋", "[green]Världen[/]");
AnsiConsole.Render(table);
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Description>Demonstrates how to render tables in a console.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -77,4 +77,7 @@ dotnet_diagnostic.CA1032.severity = none
dotnet_diagnostic.CA1826.severity = none dotnet_diagnostic.CA1826.severity = none
# RCS1079: Throwing of new NotImplementedException. # 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

@@ -34,7 +34,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="MinVer" PrivateAssets="All" Version="2.3.0" /> <PackageReference Include="MinVer" PrivateAssets="All" Version="2.3.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.0.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -1,8 +0,0 @@
root = false
[*.cs]
# Default severity for all analyzer diagnostics
dotnet_analyzer_diagnostic.severity = none
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = none

View File

@@ -1,169 +0,0 @@
using System;
using Spectre.Console;
namespace Sample
{
public static class Program
{
public static void Main(string[] args)
{
// Use the static API to write some things to the console.
AnsiConsole.Foreground = Color.Chartreuse2;
AnsiConsole.Decoration = Decoration.Underline | Decoration.Bold;
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
// to set the foreground and background color.
foreach (ConsoleColor value in Enum.GetValues(typeof(ConsoleColor)))
{
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.
var console = AnsiConsole.Console;
// Or you can build it yourself the old fashion way.
console = AnsiConsole.Create(
new AnsiConsoleSettings()
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.Standard,
Out = Console.Out,
});
// In this case, we will find the closest colors
// and downgrade them to the specified color system.
console.WriteLine();
console.Foreground = Color.Chartreuse2;
console.Decoration = Decoration.Underline | Decoration.Bold;
console.WriteLine("Hello World!");
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))
{ 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"))
{
Expand = true,
Alignment = Justify.Left,
});
// Centered panel with text
AnsiConsole.Render(new Panel(
Text.New("Centered\nCenter"))
{
Expand = true,
Alignment = Justify.Center,
});
// Right adjusted panel with text
AnsiConsole.Render(new Panel(
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 🌍");
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
table.AddRow("Lorem ipsum dolor sit amet, consectetur 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);
// 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[/]!");
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);
// 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.WriteLine();
var grid = new Grid();
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

@@ -18,6 +18,7 @@ namespace Spectre.Console.Tests
public Color Background { get; set; } public Color Background { get; set; }
public StringWriter Writer { get; } public StringWriter Writer { get; }
public string RawOutput => Writer.ToString();
public string Output => Writer.ToString().TrimEnd('\n'); public string Output => Writer.ToString().TrimEnd('\n');
public IReadOnlyList<string> Lines => Output.Split(new char[] { '\n' }); public IReadOnlyList<string> Lines => Output.Split(new char[] { '\n' });

View File

@@ -2,16 +2,17 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Shouldly" Version="4.0.0-beta0002" />
<PackageReference Include="xunit" Version="2.4.0" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="coverlet.collector" Version="1.2.0" /> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -12,7 +12,7 @@ namespace Spectre.Console.Tests.Unit
{ {
[Theory] [Theory]
[InlineData("[yellow]Hello[/]", "Hello")] [InlineData("[yellow]Hello[/]", "Hello")]
[InlineData("[yellow]Hello [italic]World[/]![/]", "Hello World!")] [InlineData("[yellow]Hello [italic]World[/]![/]", "Hello World!")]
public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected) public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected)
{ {
// Given // Given
@@ -26,7 +26,7 @@ namespace Spectre.Console.Tests.Unit
} }
[Theory] [Theory]
[InlineData("[yellow]Hello [[ World[/]", "Hello [ World")] [InlineData("[yellow]Hello [[ World[/]", "Hello [ World")]
public void Should_Be_Able_To_Escape_Tags(string markup, string expected) public void Should_Be_Able_To_Escape_Tags(string markup, string expected)
{ {
// Given // Given

View File

@@ -3,7 +3,7 @@ using Shouldly;
using Spectre.Console.Composition; using Spectre.Console.Composition;
using Xunit; using Xunit;
namespace Spectre.Console.Tests.Unit.Composition namespace Spectre.Console.Tests.Unit
{ {
public sealed class BorderTests public sealed class BorderTests
{ {

View File

@@ -1,77 +0,0 @@
using Shouldly;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class TextTests
{
[Fact]
public void Should_Render_Unstyled_Text_As_Expected()
{
// Given
var fixture = new PlainConsole(width: 80);
var text = Text.New("Hello World");
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello World");
}
[Fact]
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width()
{
// Given
var fixture = new PlainConsole(width: 5);
var text = Text.New("Hello World");
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello\n Worl\nd");
}
public sealed class TheStylizeMethod
{
[Fact]
public void Should_Apply_Style_To_Text()
{
// Given
var fixture = new AnsiConsoleFixture(ColorSystem.Standard);
var text = Text.New("Hello World");
text.Stylize(start: 3, end: 8, new Style(decoration: Decoration.Underline));
// When
fixture.Console.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello World");
}
[Fact]
public void Should_Apply_Style_To_Text_Which_Spans_Over_Multiple_Lines()
{
// Given
var fixture = new AnsiConsoleFixture(ColorSystem.Standard, width: 5);
var text = Text.New("Hello World");
text.Stylize(start: 3, end: 8, new Style(decoration: Decoration.Underline));
// When
fixture.Console.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello\n Worl\nd");
}
}
}
}

View File

@@ -2,10 +2,29 @@ using System;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace Spectre.Console.Tests.Unit.Composition namespace Spectre.Console.Tests.Unit
{ {
public sealed class GridTests 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 public sealed class TheAddRowMethod
{ {
[Fact] [Fact]
@@ -54,6 +73,53 @@ 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_Add_Empty_Row_At_The_End()
{
// Given
var console = new PlainConsole(width: 80);
var grid = new Grid();
grid.AddColumns(2);
grid.AddRow("Foo", "Bar");
grid.AddEmptyRow();
grid.AddRow("Qux", "Corgi");
grid.AddEmptyRow();
// When
console.Render(grid);
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("Foo Bar ");
console.Lines[1].ShouldBe(" ");
console.Lines[2].ShouldBe("Qux Corgi");
console.Lines[3].ShouldBe(" ");
}
}
[Fact] [Fact]
public void Should_Render_Grid_Correctly() public void Should_Render_Grid_Correctly()
{ {
@@ -99,13 +165,12 @@ namespace Spectre.Console.Tests.Unit.Composition
} }
[Fact] [Fact]
public void Should_Render_Grid_Column_Padding_Correctly() public void Should_Use_Default_Padding()
{ {
// Given // Given
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(3, 0) }); grid.AddColumns(3);
grid.AddColumns(2);
grid.AddRow("Foo", "Bar", "Baz"); grid.AddRow("Foo", "Bar", "Baz");
grid.AddRow("Qux", "Corgi", "Waldo"); grid.AddRow("Qux", "Corgi", "Waldo");
grid.AddRow("Grault", "Garply", "Fred"); grid.AddRow("Grault", "Garply", "Fred");
@@ -115,9 +180,32 @@ namespace Spectre.Console.Tests.Unit.Composition
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe(" Foo Bar Baz "); console.Lines[0].ShouldBe("Foo Bar Baz ");
console.Lines[1].ShouldBe(" Qux Corgi Waldo"); console.Lines[1].ShouldBe("Qux Corgi Waldo");
console.Lines[2].ShouldBe(" Grault Garply Fred "); 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] [Fact]

View File

@@ -12,7 +12,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New("Hello World"))); console.Render(new Panel(new Text("Hello World")));
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);
@@ -28,7 +28,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New("Hello World")) console.Render(new Panel(new Text("Hello World"))
{ {
Padding = new Padding(3, 5), Padding = new Padding(3, 5),
}); });
@@ -47,7 +47,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New(" \n💩\n "))); console.Render(new Panel(new Text(" \n💩\n ")));
// Then // Then
console.Lines.Count.ShouldBe(5); console.Lines.Count.ShouldBe(5);
@@ -65,7 +65,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New("Hello World\nFoo Bar"))); console.Render(new Panel(new Text("Hello World\nFoo Bar")));
// Then // Then
console.Lines.Count.ShouldBe(4); console.Lines.Count.ShouldBe(4);
@@ -81,7 +81,7 @@ namespace Spectre.Console.Tests.Unit
// Given // Given
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
var text = new Panel( var text = new Panel(
Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦")); Text.Markup("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
// When // When
console.Render(text); console.Render(text);
@@ -104,7 +104,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(Text.New("Hello World")) console.Render(new Panel(new Text("Hello World"))
{ {
Expand = true, Expand = true,
}); });
@@ -126,7 +126,7 @@ namespace Spectre.Console.Tests.Unit
// When // When
console.Render( console.Render(
new Panel( new Panel(
Text.New("Hello World").WithAlignment(Justify.Right)) new Text("Hello World").WithAlignment(Justify.Right))
{ {
Expand = true, Expand = true,
}); });
@@ -147,7 +147,7 @@ namespace Spectre.Console.Tests.Unit
// When // When
console.Render( console.Render(
new Panel( new Panel(
Text.New("Hello World").WithAlignment(Justify.Center)) new Text("Hello World").WithAlignment(Justify.Center))
{ {
Expand = true, Expand = true,
}); });
@@ -166,7 +166,7 @@ namespace Spectre.Console.Tests.Unit
var console = new PlainConsole(width: 80); var console = new PlainConsole(width: 80);
// When // When
console.Render(new Panel(new Panel(Text.New("Hello World")))); console.Render(new Panel(new Panel(new Text("Hello World"))));
// Then // Then
console.Lines.Count.ShouldBe(5); console.Lines.Count.ShouldBe(5);

View File

@@ -39,14 +39,18 @@ namespace Spectre.Console.Tests.Unit
[Theory] [Theory]
[InlineData("bold", Decoration.Bold)] [InlineData("bold", Decoration.Bold)]
[InlineData("b", Decoration.Bold)]
[InlineData("dim", Decoration.Dim)] [InlineData("dim", Decoration.Dim)]
[InlineData("i", Decoration.Italic)]
[InlineData("italic", Decoration.Italic)] [InlineData("italic", Decoration.Italic)]
[InlineData("underline", Decoration.Underline)] [InlineData("underline", Decoration.Underline)]
[InlineData("u", Decoration.Underline)]
[InlineData("invert", Decoration.Invert)] [InlineData("invert", Decoration.Invert)]
[InlineData("conceal", Decoration.Conceal)] [InlineData("conceal", Decoration.Conceal)]
[InlineData("slowblink", Decoration.SlowBlink)] [InlineData("slowblink", Decoration.SlowBlink)]
[InlineData("rapidblink", Decoration.RapidBlink)] [InlineData("rapidblink", Decoration.RapidBlink)]
[InlineData("strikethrough", Decoration.Strikethrough)] [InlineData("strikethrough", Decoration.Strikethrough)]
[InlineData("s", Decoration.Strikethrough)]
public void Should_Parse_Decoration(string text, Decoration decoration) public void Should_Parse_Decoration(string text, Decoration decoration)
{ {
// Given, When // Given, When
@@ -126,108 +130,83 @@ namespace Spectre.Console.Tests.Unit
result.ShouldBeOfType<InvalidOperationException>(); result.ShouldBeOfType<InvalidOperationException>();
result.Message.ShouldBe("Could not find color 'lol'."); 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 public sealed class TheTryParseMethod
{ {
[Fact] [Fact]
public void Default_Keyword_Should_Return_Default_Style() public void Should_Return_True_If_Parsing_Succeeded()
{ {
// Given, When // Given, When
var result = Style.TryParse("default", out var style); var result = Style.TryParse("bold", out var style);
// Then // Then
result.ShouldBeTrue(); result.ShouldBeTrue();
style.ShouldNotBeNull(); style.ShouldNotBeNull();
style.Foreground.ShouldBe(Color.Default); style.Decoration.ShouldBe(Decoration.Bold);
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);
} }
[Fact] [Fact]
public void Should_Parse_Text_And_Decoration() public void Should_Return_False_If_Parsing_Failed()
{ {
// Given, When // Given, When
var result = Style.TryParse("bold underline blue on green", out var style); var result = Style.TryParse("lol", out _);
// 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 _);
// Then // Then
result.ShouldBeFalse(); result.ShouldBeFalse();

View File

@@ -2,7 +2,7 @@ using System;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace Spectre.Console.Tests.Unit.Composition namespace Spectre.Console.Tests.Unit
{ {
public sealed class TableTests public sealed class TableTests
{ {
@@ -21,6 +21,22 @@ namespace Spectre.Console.Tests.Unit.Composition
result.ShouldBeOfType<ArgumentNullException>() result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("column"); .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 public sealed class TheAddColumnsMethod
@@ -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] [Fact]
public void Should_Render_Table_Correctly() public void Should_Render_Table_Correctly()
{ {
@@ -254,9 +298,9 @@ namespace Spectre.Console.Tests.Unit.Composition
// Then // Then
console.Lines.Count.ShouldBe(3); console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("Foo Bar Baz "); console.Lines[0].ShouldBe("Foo Bar Baz ");
console.Lines[1].ShouldBe("Qux Corgi Waldo"); console.Lines[1].ShouldBe("Qux Corgi Waldo");
console.Lines[2].ShouldBe("Grault Garply Fred "); console.Lines[2].ShouldBe("Grault Garply Fred ");
} }
[Fact] [Fact]
@@ -307,5 +351,24 @@ namespace Spectre.Console.Tests.Unit.Composition
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │"); console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
console.Lines[6].ShouldBe("└────────┴────────┴──────────┘"); 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

@@ -0,0 +1,87 @@
using System.Text;
using Shouldly;
using Spectre.Console.Composition;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class TextTests
{
public sealed class Measuring
{
[Fact]
public void Should_Return_The_Longest_Word_As_Minimum_Width()
{
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
result.Min.ShouldBe(6);
}
[Fact]
public void Should_Return_The_Longest_Line_As_Maximum_Width()
{
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
result.Max.ShouldBe(11);
}
}
public sealed class Rendering
{
[Fact]
public void Should_Render_Unstyled_Text_As_Expected()
{
// Given
var fixture = new PlainConsole(width: 80);
var text = new Text("Hello World");
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe("Hello World");
}
[Theory]
[InlineData("Hello\n\nWorld\n\n")]
[InlineData("Hello\r\n\r\nWorld\r\n\r\n")]
public void Should_Write_Line_Breaks(string input)
{
// Given
var fixture = new PlainConsole(width: 5);
var text = new Text(input);
// When
fixture.Render(text);
// Then
fixture.RawOutput.ShouldBe("Hello\n\nWorld\n\n");
}
[Theory]
[InlineData(5, "Hello World", "Hello\nWorld")]
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width(
int width, string input, string expected)
{
// Given
var fixture = new PlainConsole(width);
var text = new Text(input);
// When
fixture.Render(text);
// Then
fixture.Output
.NormalizeLineEndings()
.ShouldBe(expected);
}
}
}
}

View File

@@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "Spectre.
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Tests", "Spectre.Console.Tests\Spectre.Console.Tests.csproj", "{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Tests", "Spectre.Console.Tests\Spectre.Console.Tests.csproj", "{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{272E6092-BD31-4EB6-A9FF-F4179F91958F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
@@ -17,6 +15,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
stylecop.json = stylecop.json stylecop.json = stylecop.json
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{F0575243-121F-4DEE-9F6B-246E26DC0844}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Table", "..\examples\Table\Table.csproj", "{94ECCBA8-7EBF-4B53-8379-52EB2327417E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Panel", "..\examples\Panel\Panel.csproj", "{BFF37228-B376-4ADD-9657-4E501F929713}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grid", "..\examples\Grid\Grid.csproj", "{C7FF6FDB-FB59-4517-8669-521C96AB7323}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors", "..\examples\Colors\Colors.csproj", "{1F51C55C-BA4C-4856-9001-0F7924FFB179}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -51,22 +59,64 @@ Global
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x64.Build.0 = Release|Any CPU {9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x64.Build.0 = Release|Any CPU
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.ActiveCfg = Release|Any CPU {9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.ActiveCfg = Release|Any CPU
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.Build.0 = Release|Any CPU {9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.Build.0 = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|Any CPU.Build.0 = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x64.ActiveCfg = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|x64.ActiveCfg = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x64.Build.0 = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|x64.Build.0 = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x86.ActiveCfg = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|x86.ActiveCfg = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x86.Build.0 = Debug|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|x86.Build.0 = Debug|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|Any CPU.ActiveCfg = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|Any CPU.Build.0 = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|Any CPU.Build.0 = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x64.ActiveCfg = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|x64.ActiveCfg = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x64.Build.0 = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|x64.Build.0 = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x86.ActiveCfg = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|x86.ActiveCfg = Release|Any CPU
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x86.Build.0 = Release|Any CPU {94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Release|x86.Build.0 = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|x64.ActiveCfg = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|x64.Build.0 = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|x86.ActiveCfg = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Debug|x86.Build.0 = Debug|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|Any CPU.Build.0 = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|x64.ActiveCfg = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|x64.Build.0 = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|x86.ActiveCfg = Release|Any CPU
{BFF37228-B376-4ADD-9657-4E501F929713}.Release|x86.Build.0 = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|x64.ActiveCfg = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|x64.Build.0 = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|x86.ActiveCfg = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Debug|x86.Build.0 = Debug|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|Any CPU.Build.0 = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|x64.ActiveCfg = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|x64.Build.0 = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|x86.ActiveCfg = Release|Any CPU
{C7FF6FDB-FB59-4517-8669-521C96AB7323}.Release|x86.Build.0 = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|x64.ActiveCfg = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|x64.Build.0 = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|x86.ActiveCfg = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Debug|x86.Build.0 = Debug|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|Any CPU.Build.0 = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|x64.ActiveCfg = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|x64.Build.0 = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|x86.ActiveCfg = Release|Any CPU
{1F51C55C-BA4C-4856-9001-0F7924FFB179}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{94ECCBA8-7EBF-4B53-8379-52EB2327417E} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{BFF37228-B376-4ADD-9657-4E501F929713} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{C7FF6FDB-FB59-4517-8669-521C96AB7323} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{1F51C55C-BA4C-4856-9001-0F7924FFB179} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
EndGlobalSection EndGlobalSection

View File

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

View File

@@ -50,7 +50,7 @@ namespace Spectre.Console
ColorSystem.Standard => "4 bits", ColorSystem.Standard => "4 bits",
ColorSystem.EightBit => "8 bits", ColorSystem.EightBit => "8 bits",
ColorSystem.TrueColor => "24 bits", ColorSystem.TrueColor => "24 bits",
_ => "?" _ => "?",
}; };
return $"ANSI={supportsAnsi}, Colors={ColorSystem}, Kind={legacyConsole} ({bits})"; return $"ANSI={supportsAnsi}, Colors={ColorSystem}, Kind={legacyConsole} ({bits})";

View File

@@ -74,7 +74,7 @@ namespace Spectre.Console
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
return obj is Color color && Equals(color); return obj is Color color && Equals(color);
} }

View File

@@ -57,15 +57,20 @@ namespace Spectre.Console.Composition
private Dictionary<BorderPart, string> Initialize() private Dictionary<BorderPart, string> Initialize()
{ {
var lookup = new Dictionary<BorderPart, string>(); 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) if (text.Length > 1)
{ {
throw new InvalidOperationException("A box part cannot contain more than one character."); 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; return lookup;

View File

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

View File

@@ -29,7 +29,7 @@ namespace Spectre.Console.Composition
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
return obj is Measurement measurement && Equals(measurement); return obj is Measurement measurement && Equals(measurement);
} }

View File

@@ -29,7 +29,7 @@ namespace Spectre.Console
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
return obj is Padding padding && Equals(padding); return obj is Padding padding && Equals(padding);
} }

View File

@@ -24,11 +24,28 @@ namespace Spectre.Console.Composition
/// </summary> /// </summary>
public bool IsLineBreak { get; } public bool IsLineBreak { get; }
/// <summary>
/// Gets a value indicating whether or not this is a whitespace
/// that should be preserved but not taken into account when
/// layouting text.
/// </summary>
public bool IsWhiteSpace { get; }
/// <summary> /// <summary>
/// Gets the segment style. /// Gets the segment style.
/// </summary> /// </summary>
public Style Style { get; } public Style Style { get; }
/// <summary>
/// Gets a segment representing a line break.
/// </summary>
public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true);
/// <summary>
/// Gets an empty segment.
/// </summary>
public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class. /// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary> /// </summary>
@@ -50,18 +67,15 @@ namespace Spectre.Console.Composition
private Segment(string text, Style style, bool lineBreak) 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; Style = style;
IsLineBreak = lineBreak; IsLineBreak = lineBreak;
} IsWhiteSpace = string.IsNullOrWhiteSpace(text);
/// <summary>
/// Creates a segment that represents an implicit line break.
/// </summary>
/// <returns>A segment that represents an implicit line break.</returns>
public static Segment LineBreak()
{
return new Segment("\n", Style.Plain, true);
} }
/// <summary> /// <summary>
@@ -81,7 +95,7 @@ namespace Spectre.Console.Composition
/// <returns>A new segment without any trailing line endings.</returns> /// <returns>A new segment without any trailing line endings.</returns>
public Segment StripLineEndings() public Segment StripLineEndings()
{ {
return new Segment(Text.TrimEnd('\n'), Style); return new Segment(Text.TrimEnd('\n').TrimEnd('\r'), Style);
} }
/// <summary> /// <summary>
@@ -89,7 +103,7 @@ namespace Spectre.Console.Composition
/// </summary> /// </summary>
/// <param name="offset">The offset where to split the segment.</param> /// <param name="offset">The offset where to split the segment.</param>
/// <returns>One or two new segments representing the split.</returns> /// <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) if (offset < 0)
{ {
@@ -138,9 +152,9 @@ namespace Spectre.Console.Composition
{ {
var segment = stack.Pop(); var segment = stack.Pop();
if (line.Length + segment.Text.Length > maxWidth) if (line.Width + segment.Text.Length > maxWidth)
{ {
var diff = -(maxWidth - (line.Length + segment.Text.Length)); var diff = -(maxWidth - (line.Width + segment.Text.Length));
var offset = segment.Text.Length - diff; var offset = segment.Text.Length - diff;
var (first, second) = segment.Split(offset); var (first, second) = segment.Split(offset);
@@ -161,7 +175,7 @@ namespace Spectre.Console.Composition
{ {
if (segment.Text == "\n") if (segment.Text == "\n")
{ {
if (line.Length > 0 || segment.IsLineBreak) if (line.Width > 0 || segment.IsLineBreak)
{ {
lines.Add(line); lines.Add(line);
line = new SegmentLine(); line = new SegmentLine();
@@ -184,7 +198,7 @@ namespace Spectre.Console.Composition
if (parts.Length > 1) if (parts.Length > 1)
{ {
if (line.Length > 0) if (line.Width > 0)
{ {
lines.Add(line); lines.Add(line);
line = new SegmentLine(); line = new SegmentLine();

View File

@@ -1,18 +1,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text;
namespace Spectre.Console.Composition namespace Spectre.Console.Composition
{ {
/// <summary> /// <summary>
/// Represents a line of segments. /// Represents a collection of segments.
/// </summary> /// </summary>
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")] [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")]
public sealed class SegmentLine : List<Segment> public sealed class SegmentLine : List<Segment>
{ {
/// <summary> /// <summary>
/// Gets the length of the line. /// Gets the width of the line.
/// </summary> /// </summary>
public int Length => this.Sum(line => line.Text.Length); public int Width => this.Sum(line => line.Text.Length);
/// <summary>
/// Gets the cell width of the segment line.
/// </summary>
/// <param name="encoding">The encoding to use.</param>
/// <returns>The cell width of the segment line.</returns>
public int CellWidth(Encoding encoding)
{
return this.Sum(line => line.CellLength(encoding));
}
/// <summary>
/// Preprends a segment to the line.
/// </summary>
/// <param name="segment">The segment to prepend.</param>
public void Prepend(Segment segment)
{
Insert(0, segment);
}
} }
} }

View File

@@ -0,0 +1,25 @@
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Composition
{
internal sealed class SegmentLineEnumerator : IEnumerable<Segment>
{
private readonly List<SegmentLine> _lines;
public SegmentLineEnumerator(List<SegmentLine> lines)
{
_lines = lines;
}
public IEnumerator<Segment> GetEnumerator()
{
return new SegmentLineIterator(_lines);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -0,0 +1,99 @@
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Composition
{
internal sealed class SegmentLineIterator : IEnumerator<Segment>
{
private readonly List<SegmentLine> _lines;
private int _currentLine;
private int _currentIndex;
private bool _lineBreakEmitted;
public Segment Current { get; private set; }
object? IEnumerator.Current => Current;
public SegmentLineIterator(List<SegmentLine> lines)
{
_currentLine = 0;
_currentIndex = -1;
_lines = lines;
Current = Segment.Empty;
}
public void Dispose()
{
}
public bool MoveNext()
{
if (_currentLine > _lines.Count - 1)
{
return false;
}
_currentIndex += 1;
// Did we go past the end of the line?
if (_currentIndex > _lines[_currentLine].Count - 1)
{
// We haven't just emitted a line break?
if (!_lineBreakEmitted)
{
// Got any more lines?
if (_currentIndex + 1 > _lines[_currentLine].Count - 1)
{
// Only emit a line break if the next one isn't a line break.
if ((_currentLine + 1 <= _lines.Count - 1)
&& _lines[_currentLine + 1].Count > 0
&& !_lines[_currentLine + 1][0].IsLineBreak)
{
_lineBreakEmitted = true;
Current = Segment.LineBreak;
return true;
}
}
}
// Increase the line and reset the index.
_currentLine += 1;
_currentIndex = 0;
_lineBreakEmitted = false;
// No more lines?
if (_currentLine > _lines.Count - 1)
{
return false;
}
// Nothing on the line?
while (_currentIndex > _lines[_currentLine].Count - 1)
{
_currentLine += 1;
_currentIndex = 0;
if (_currentLine > _lines.Count - 1)
{
return false;
}
}
}
// Reset the flag
_lineBreakEmitted = false;
Current = _lines[_currentLine][_currentIndex];
return true;
}
public void Reset()
{
_currentLine = 0;
_currentIndex = -1;
Current = Segment.Empty;
}
}
}

View File

@@ -1,285 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents text with color and decorations.
/// </summary>
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
[DebuggerDisplay("{_text,nq}")]
public sealed class Text : IRenderable
{
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; }
public int End { get; }
public Style Style { get; }
public Span(int start, int end, Style style)
{
Start = start;
End = end;
Style = style ?? Style.Plain;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Console.Text"/> class.
/// </summary>
/// <param name="text">The text.</param>
internal Text(string text)
{
_text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
_spans = new List<Span>();
}
/// <summary>
/// Initializes a new instance of the <see cref="Text"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="foreground">The foreground.</param>
/// <param name="background">The background.</param>
/// <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)
{
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>
/// <param name="text">The text to append.</param>
/// <param name="style">The text style.</param>
public void Append(string text, Style style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
var start = _text.Length;
var end = _text.Length + text.Length;
_text += text;
Stylize(start, end, style);
}
/// <summary>
/// Stylizes a part of the text.
/// </summary>
/// <param name="start">The start position.</param>
/// <param name="end">The end position.</param>
/// <param name="style">The style to apply.</param>
public void Stylize(int start, int end, Style style)
{
if (start >= end)
{
throw new ArgumentOutOfRangeException(nameof(start), "Start position must be less than the end position.");
}
start = Math.Max(start, 0);
end = Math.Min(end, _text.Length);
_spans.Add(new Span(start, end, style));
}
/// <inheritdoc/>
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
{
if (string.IsNullOrEmpty(_text))
{
return new Measurement(1, 1);
}
// 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/>
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
{
if (string.IsNullOrWhiteSpace(_text))
{
return Array.Empty<Segment>();
}
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());
}
// 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)
{
result.Add(Segment.LineBreak());
}
}
return result;
}
private IEnumerable<Segment> SplitLineBreaks(IEnumerable<Segment> segments)
{
// Creates individual segments of line breaks.
var result = new List<Segment>();
var queue = new Stack<Segment>(segments.Reverse());
while (queue.Count > 0)
{
var segment = queue.Pop();
var index = segment.Text.IndexOf("\n", StringComparison.OrdinalIgnoreCase);
if (index == -1)
{
result.Add(segment);
}
else
{
var (first, second) = segment.Split(index);
if (!string.IsNullOrEmpty(first.Text))
{
result.Add(first);
}
result.Add(Segment.LineBreak());
queue.Push(new Segment(second.Text.Substring(1), second.Style));
}
}
return result;
}
private IEnumerable<Segment> CreateSegments()
{
// This excellent algorithm to sort spans was ported and adapted from
// https://github.com/willmcgugan/rich/blob/eb2f0d5277c159d8693636ec60c79c5442fd2e43/rich/text.py#L492
// Create the style map.
var styleMap = _spans.SelectIndex((span, index) => (span, index)).ToDictionary(x => x.index + 1, x => x.span.Style);
styleMap[0] = Style.Plain;
// Create a span list.
var spans = new List<(int Offset, bool Leaving, int Style)>();
spans.AddRange(_spans.SelectIndex((span, index) => (span.Start, false, index + 1)));
spans.AddRange(_spans.SelectIndex((span, index) => (span.End, true, index + 1)));
spans = spans.OrderBy(x => x.Offset).ThenBy(x => !x.Leaving).ToList();
// Keep track of applied styles using a stack
var styleStack = new Stack<int>();
// Now build the segments.
var result = new List<Segment>();
foreach (var (offset, leaving, style, nextOffset) in BuildSkipList(spans))
{
if (leaving)
{
// Leaving
styleStack.Pop();
}
else
{
// Entering
styleStack.Push(style);
}
if (nextOffset > offset)
{
// Build the current style from the stack
var styleIndices = styleStack.OrderBy(index => index).ToArray();
var currentStyle = Style.Plain.Combine(styleIndices.Select(index => styleMap[index]));
// Create segment
var text = _text.Substring(offset, Math.Min(_text.Length - offset, nextOffset - offset));
result.Add(new Segment(text, currentStyle));
}
}
return result;
}
private static IEnumerable<(int Offset, bool Leaving, int Style, int NextOffset)> BuildSkipList(
List<(int Offset, bool Leaving, int Style)> spans)
{
return spans.Zip(spans.Skip(1), (first, second) => (first, second)).Select(
x => (x.first.Offset, x.first.Leaving, x.first.Style, NextOffset: x.second.Offset));
}
}
}

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Composition; using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console namespace Spectre.Console
{ {
@@ -21,6 +23,7 @@ namespace Spectre.Console
Border = BorderKind.None, Border = BorderKind.None,
ShowHeaders = false, ShowHeaders = false,
IsGrid = true, IsGrid = true,
PadRightCell = false,
}; };
} }
@@ -55,11 +58,19 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column)); 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) _table.AddColumn(new TableColumn(string.Empty)
{ {
Width = column.Width, Width = column.Width,
NoWrap = column.NoWrap, NoWrap = column.NoWrap,
Padding = column.Padding, Padding = column.Padding ?? new Padding(0, 2),
Alignment = column.Alignment, Alignment = column.Alignment,
}); });
} }
@@ -93,6 +104,16 @@ namespace Spectre.Console
} }
} }
/// <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> /// <summary>
/// Adds a new row to the grid. /// Adds a new row to the grid.
/// </summary> /// </summary>

View File

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

View File

@@ -9,6 +9,8 @@ namespace Spectre.Console
/// </summary> /// </summary>
public sealed class Panel : IRenderable public sealed class Panel : IRenderable
{ {
private const int EdgeWidth = 2;
private readonly IRenderable _child; private readonly IRenderable _child;
/// <summary> /// <summary>
@@ -26,14 +28,14 @@ namespace Spectre.Console
/// <summary> /// <summary>
/// Gets or sets the alignment of the panel contents. /// Gets or sets the alignment of the panel contents.
/// </summary> /// </summary>
public Justify? Alignment { get; set; } = null; public Justify? Alignment { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the panel should /// 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 /// fit the available space. If <c>false</c>, the panel width will be
/// auto calculated. Defaults to <c>false</c>. /// auto calculated. Defaults to <c>false</c>.
/// </summary> /// </summary>
public bool Expand { get; set; } = false; public bool Expand { get; set; }
/// <summary> /// <summary>
/// Gets or sets the padding. /// Gets or sets the padding.
@@ -61,13 +63,12 @@ namespace Spectre.Console
{ {
var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder); var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var edgeWidth = 2;
var paddingWidth = Padding.GetHorizontalPadding(); var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = width - edgeWidth - paddingWidth; var childWidth = width - EdgeWidth - paddingWidth;
if (!Expand) if (!Expand)
{ {
var measurement = _child.Measure(context, width - edgeWidth - paddingWidth); var measurement = _child.Measure(context, width - EdgeWidth - paddingWidth);
childWidth = measurement.Max; childWidth = measurement.Max;
} }
@@ -79,7 +80,7 @@ namespace Spectre.Console
new Segment(border.GetPart(BorderPart.HeaderTopLeft)), new Segment(border.GetPart(BorderPart.HeaderTopLeft)),
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth)), new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth)),
new Segment(border.GetPart(BorderPart.HeaderTopRight)), new Segment(border.GetPart(BorderPart.HeaderTopRight)),
new Segment("\n"), Segment.LineBreak,
}; };
// Render the child. // Render the child.
@@ -117,14 +118,14 @@ namespace Spectre.Console
} }
result.Add(new Segment(border.GetPart(BorderPart.CellRight))); result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
result.Add(new Segment("\n")); result.Add(Segment.LineBreak);
} }
// Panel bottom // Panel bottom
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth)));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
result.Add(new Segment("\n")); result.Add(Segment.LineBreak);
return result; return result;
} }

View File

@@ -11,6 +11,8 @@ namespace Spectre.Console
/// </summary> /// </summary>
public sealed partial class Table public sealed partial class Table
{ {
private const int EdgeCount = 2;
// Calculate the widths of each column, including padding, not including borders. // Calculate the widths of each column, including padding, not including borders.
// Ported from Rich by Will McGugan, licensed under MIT. // Ported from Rich by Will McGugan, licensed under MIT.
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394 // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
@@ -61,7 +63,7 @@ namespace Spectre.Console
.Where(x => x.allowWrap) .Where(x => x.allowWrap)
.Max(x => x.width); .Max(x => x.width);
var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 0).Max(); var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
var columnDifference = maxColumn - secondMaxColumn; var columnDifference = maxColumn - secondMaxColumn;
var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList(); var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
@@ -96,9 +98,15 @@ namespace Spectre.Console
var minWidths = new List<int>(); var minWidths = new List<int>();
var maxWidths = 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) foreach (var row in rows)
{ {
var measure = ((IRenderable)row).Measure(options, maxWidth); measure = ((IRenderable)row).Measure(options, maxWidth);
minWidths.Add(measure.Min); minWidths.Add(measure.Min);
maxWidths.Add(measure.Max); maxWidths.Add(measure.Max);
} }
@@ -109,10 +117,9 @@ namespace Spectre.Console
private int GetExtraWidth(bool includePadding) private int GetExtraWidth(bool includePadding)
{ {
var edges = 2;
var separators = _columns.Count - 1; var separators = _columns.Count - 1;
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0; var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
return separators + edges + padding; return separators + EdgeCount + padding;
} }
} }
} }

View File

@@ -39,12 +39,12 @@ namespace Spectre.Console
/// fit the available space. If <c>false</c>, the table width will be /// fit the available space. If <c>false</c>, the table width will be
/// auto calculated. Defaults to <c>false</c>. /// auto calculated. Defaults to <c>false</c>.
/// </summary> /// </summary>
public bool Expand { get; set; } = false; public bool Expand { get; set; }
/// <summary> /// <summary>
/// Gets or sets the width of the table. /// Gets or sets the width of the table.
/// </summary> /// </summary>
public int? Width { get; set; } = null; public int? Width { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not to use /// Gets or sets a value indicating whether or not to use
@@ -53,7 +53,13 @@ namespace Spectre.Console
/// </summary> /// </summary>
public bool SafeBorder { get; set; } = true; public bool SafeBorder { get; set; } = true;
internal bool IsGrid { get; set; } = false; // 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> /// <summary>
/// Initializes a new instance of the <see cref="Table"/> class. /// Initializes a new instance of the <see cref="Table"/> class.
@@ -75,7 +81,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column)); throw new ArgumentNullException(nameof(column));
} }
_columns.Add(new TableColumn(column)); AddColumn(new TableColumn(column));
} }
/// <summary> /// <summary>
@@ -89,6 +95,11 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column)); throw new ArgumentNullException(nameof(column));
} }
if (_rows.Count > 0)
{
throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
}
_columns.Add(column); _columns.Add(column);
} }
@@ -103,7 +114,10 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns)); throw new ArgumentNullException(nameof(columns));
} }
_columns.AddRange(columns.Select(column => new TableColumn(column))); foreach (var column in columns)
{
AddColumn(column);
}
} }
/// <summary> /// <summary>
@@ -117,7 +131,20 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns)); throw new ArgumentNullException(nameof(columns));
} }
_columns.AddRange(columns.Select(column => column)); 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> /// <summary>
@@ -141,7 +168,7 @@ namespace Spectre.Console
throw new InvalidOperationException("The number of row columns are greater than the number of table columns."); throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
} }
_rows.Add(columns.Select(column => Text.New(column)).ToList()); _rows.Add(columns.Select(column => Text.Markup(column)).ToList());
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -178,6 +205,7 @@ namespace Spectre.Console
var showBorder = Border != BorderKind.None; var showBorder = Border != BorderKind.None;
var hideBorder = Border == BorderKind.None; var hideBorder = Border == BorderKind.None;
var hasRows = _rows.Count > 0;
var maxWidth = width; var maxWidth = width;
if (Width != null) if (Width != null)
@@ -240,7 +268,7 @@ namespace Spectre.Console
} }
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight))); result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight)));
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak);
} }
// Iterate through each cell row // Iterate through each cell row
@@ -278,7 +306,7 @@ namespace Spectre.Console
} }
// Pad column on the right side // Pad column on the right side
if (showBorder || (hideBorder && !lastCell) || (IsGrid && !lastCell)) if (showBorder || (hideBorder && !lastCell) || (hideBorder && lastCell && IsGrid && PadRightCell))
{ {
var rightPadding = _columns[cellIndex].Padding.Right; var rightPadding = _columns[cellIndex].Padding.Right;
if (rightPadding > 0) if (rightPadding > 0)
@@ -292,18 +320,18 @@ namespace Spectre.Console
// Add right column edge // Add right column edge
result.Add(new Segment(border.GetPart(BorderPart.CellRight))); result.Add(new Segment(border.GetPart(BorderPart.CellRight)));
} }
else if (showBorder || (hideBorder && !lastCell)) else if (showBorder)
{ {
// Add column separator // Add column separator
result.Add(new Segment(border.GetPart(BorderPart.CellSeparator))); result.Add(new Segment(border.GetPart(BorderPart.CellSeparator)));
} }
} }
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak);
} }
// Show header separator? // Show header separator?
if (firstRow && showBorder && ShowHeaders) 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()) foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
@@ -321,7 +349,7 @@ namespace Spectre.Console
} }
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight))); result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight)));
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak);
} }
// Show bottom of footer? // Show bottom of footer?
@@ -343,7 +371,7 @@ namespace Spectre.Console
} }
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight))); result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
result.Add(Segment.LineBreak()); result.Add(Segment.LineBreak);
} }
} }

View File

@@ -40,7 +40,7 @@ namespace Spectre.Console
/// <param name="text">The table column text.</param> /// <param name="text">The table column text.</param>
public TableColumn(string text) public TableColumn(string text)
{ {
Text = Text.New(text ?? throw new ArgumentNullException(nameof(text))); Text = Text.Markup(text ?? throw new ArgumentNullException(nameof(text)));
Width = null; Width = null;
Padding = new Padding(1, 1); Padding = new Padding(1, 1);
NoWrap = false; NoWrap = false;

View File

@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Composition;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a piece of text.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
public sealed class Text : IRenderable
{
private readonly List<SegmentLine> _lines;
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public Justify Alignment { get; set; } = Justify.Left;
/// <summary>
/// Initializes a new instance of the <see cref="Text"/> class.
/// </summary>
public Text()
{
_lines = new List<SegmentLine>();
}
/// <summary>
/// Initializes a new instance of the <see cref="Text"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="style">The style of the text.</param>
public Text(string text, Style? style = null)
: this()
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
Append(text, style);
}
/// <summary>
/// Creates a <see cref="Text"/> instance representing
/// the specified markup text.
/// </summary>
/// <param name="text">The markup text.</param>
/// <param name="style">The text style.</param>
/// <returns>a <see cref="Text"/> instance representing the specified markup text.</returns>
public static Text Markup(string text, Style? style = null)
{
var result = MarkupParser.Parse(text, style ?? Style.Plain);
return result;
}
/// <inheritdoc/>
public Measurement Measure(RenderContext context, int maxWidth)
{
if (_lines.Count == 0)
{
return new Measurement(0, 0);
}
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
var max = _lines.Max(x => x.CellWidth(context.Encoding));
return new Measurement(min, max);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (_lines.Count == 0)
{
return Array.Empty<Segment>();
}
var lines = SplitLines(context, maxWidth);
// Justify lines
var justification = context.Justification ?? Alignment;
foreach (var (_, _, last, line) in lines.Enumerate())
{
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
if (length < maxWidth)
{
// Justify right side
if (justification == Justify.Right)
{
var diff = maxWidth - length;
line.Prepend(new Segment(new string(' ', diff)));
}
else if (justification == Justify.Center)
{
// Left side.
var diff = (maxWidth - length) / 2;
line.Prepend(new Segment(new string(' ', diff)));
// Right side
line.Add(new Segment(new string(' ', diff)));
var remainder = (maxWidth - length) % 2;
if (remainder != 0)
{
line.Add(new Segment(new string(' ', remainder)));
}
}
}
}
return new SegmentLineEnumerator(lines);
}
/// <summary>
/// Appends a piece of text.
/// </summary>
/// <param name="text">The text to append.</param>
/// <param name="style">The style of the appended text.</param>
public void Append(string text, Style? style = null)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
{
var current = part;
if (first)
{
var line = _lines.LastOrDefault();
if (line == null)
{
_lines.Add(new SegmentLine());
line = _lines.Last();
}
if (string.IsNullOrEmpty(current))
{
line.Add(Segment.Empty);
}
else
{
foreach (var span in current.SplitWords())
{
line.Add(new Segment(span, style ?? Style.Plain));
}
}
}
else
{
var line = new SegmentLine();
if (string.IsNullOrEmpty(current))
{
line.Add(Segment.Empty);
}
else
{
foreach (var span in current.SplitWords())
{
line.Add(new Segment(span, style ?? Style.Plain));
}
}
_lines.Add(line);
}
}
}
private List<SegmentLine> Clone()
{
var result = new List<SegmentLine>();
foreach (var line in _lines)
{
var newLine = new SegmentLine();
foreach (var segment in line)
{
newLine.Add(segment);
}
result.Add(newLine);
}
return result;
}
private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
{
if (_lines.Max(x => x.CellWidth(context.Encoding)) <= maxWidth)
{
return Clone();
}
var lines = new List<SegmentLine>();
var line = new SegmentLine();
var newLine = true;
using (var iterator = new SegmentLineIterator(_lines))
{
while (iterator.MoveNext())
{
var current = iterator.Current;
if (current == null)
{
throw new InvalidOperationException("Iterator returned empty segment.");
}
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
{
newLine = false;
continue;
}
newLine = false;
if (current.IsLineBreak)
{
line.Add(current);
lines.Add(line);
line = new SegmentLine();
newLine = true;
continue;
}
var length = current.CellLength(context.Encoding);
if (line.CellWidth(context.Encoding) + length > maxWidth)
{
line.Add(Segment.Empty);
lines.Add(line);
line = new SegmentLine();
newLine = true;
}
if (newLine && current.IsWhiteSpace)
{
continue;
}
newLine = false;
line.Add(current);
}
}
// Flush remaining.
if (line.Count > 0)
{
lines.Add(line);
}
return lines;
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Text"/>.
/// </summary>
public static class TextExtensions
{
/// <summary>
/// Sets the text alignment.
/// </summary>
/// <param name="text">The <see cref="Text"/> instance.</param>
/// <param name="alignment">The text alignment.</param>
/// <returns>The same <see cref="Text"/> instance.</returns>
public static Text WithAlignment(this Text text, Justify alignment)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
text.Alignment = alignment;
return text;
}
}
}

View File

@@ -28,17 +28,24 @@ namespace Spectre.Console
var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole); var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
foreach (var segment in renderable.Render(options, console.Width)) using (console.PushStyle(Style.Plain))
{ {
if (!segment.Style.Equals(Style.Plain)) var current = Style.Plain;
foreach (var segment in renderable.Render(options, console.Width))
{ {
using (var style = console.PushStyle(segment.Style)) if (string.IsNullOrEmpty(segment.Text))
{ {
console.Write(segment.Text); continue;
} }
}
else if (!segment.Style.Equals(current))
{ {
console.Foreground = segment.Style.Foreground;
console.Background = segment.Style.Background;
console.Decoration = segment.Style.Decoration;
current = segment.Style;
}
console.Write(segment.Text); console.Write(segment.Text);
} }
} }

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
namespace Spectre.Console.Internal namespace Spectre.Console.Internal
@@ -17,14 +18,9 @@ namespace Spectre.Console.Internal
public static string NormalizeLineEndings(this string text, bool native = false) public static string NormalizeLineEndings(this string text, bool native = false)
{ {
if (text == null) text ??= string.Empty;
{
return null;
}
var normalized = text?.Replace("\r\n", "\n")
?.Replace("\r", string.Empty);
var normalized = text?.Replace("\r\n", "\n")?.Replace("\r", string.Empty) ?? string.Empty;
if (native && !_alreadyNormalized) if (native && !_alreadyNormalized)
{ {
normalized = normalized.Replace("\n", Environment.NewLine); normalized = normalized.Replace("\n", Environment.NewLine);
@@ -35,7 +31,52 @@ namespace Spectre.Console.Internal
public static string[] SplitLines(this string text) 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>();
}
public static string[] SplitWords(this string word, StringSplitOptions options = StringSplitOptions.None)
{
var result = new List<string>();
static string Read(StringBuffer reader, Func<char, bool> criteria)
{
var buffer = new StringBuilder();
while (!reader.Eof)
{
var current = reader.Peek();
if (!criteria(current))
{
break;
}
buffer.Append(reader.Read());
}
return buffer.ToString();
}
using (var reader = new StringBuffer(word))
{
while (!reader.Eof)
{
var current = reader.Peek();
if (char.IsWhiteSpace(current))
{
var x = Read(reader, c => char.IsWhiteSpace(c));
if (options != StringSplitOptions.RemoveEmptyEntries)
{
result.Add(x);
}
}
else
{
result.Add(Read(reader, c => !char.IsWhiteSpace(c)));
}
}
}
return result.ToArray();
} }
} }
} }

View File

@@ -70,6 +70,11 @@ namespace Spectre.Console.Internal
public static int GetCellLength(Encoding encoding, char rune) public static int GetCellLength(Encoding encoding, char rune)
{ {
if (rune == '\r' || rune == '\n')
{
return 0;
}
// Is it represented by a single byte? // Is it represented by a single byte?
// In that case we don't have to calculate the // In that case we don't have to calculate the
// actual cell width. // actual cell width.

View File

@@ -6,11 +6,11 @@ namespace Spectre.Console.Internal
{ {
internal static class MarkupParser 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; style ??= Style.Plain;
var result = new Text(string.Empty); var result = new Text();
using var tokenizer = new MarkupTokenizer(text); using var tokenizer = new MarkupTokenizer(text);
var stack = new Stack<Style>(); var stack = new Stack<Style>();
@@ -18,6 +18,10 @@ namespace Spectre.Console.Internal
while (tokenizer.MoveNext()) while (tokenizer.MoveNext())
{ {
var token = tokenizer.Current; var token = tokenizer.Current;
if (token == null)
{
break;
}
if (token.Kind == MarkupTokenKind.Open) if (token.Kind == MarkupTokenKind.Open)
{ {

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Spectre.Console.Internal namespace Spectre.Console.Internal
{ {
@@ -12,16 +14,23 @@ namespace Spectre.Console.Internal
throw new InvalidOperationException(error); 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; 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); style = Parse(text, out var error);
return error == null; 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 effectiveDecoration = (Decoration?)null;
var effectiveForeground = (Color?)null; var effectiveForeground = (Color?)null;
@@ -57,16 +66,30 @@ namespace Spectre.Console.Internal
var color = ColorTable.GetColor(part); var color = ColorTable.GetColor(part);
if (color == null) 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 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) if (foreground)
@@ -95,5 +118,82 @@ namespace Spectre.Console.Internal
error = null; error = null;
return new Style(effectiveForeground, effectiveBackground, effectiveDecoration); 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

@@ -40,7 +40,7 @@ namespace Spectre.Console.Internal
return result; return result;
} }
public static List<int> Distribute(int total, List<int> ratios, List<int> minimums = null) public static List<int> Distribute(int total, List<int> ratios, List<int>? minimums = null)
{ {
if (minimums != null) if (minimums != null)
{ {

View File

@@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -9,27 +10,21 @@
<None Include="../../gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" /> <None Include="../../gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup> </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>
<Compile Update="**/Table.*.cs">
<DependentUpon>**/Table.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.4" /> <PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup> </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> </Project>

View File

@@ -57,6 +57,15 @@ namespace Spectre.Console
return StyleParser.Parse(text); return StyleParser.Parse(text);
} }
/// <summary>
/// Creates a copy of the current <see cref="Style"/>.
/// </summary>
/// <returns>A copy of the current <see cref="Style"/>.</returns>
public Style Clone()
{
return new Style(Foreground, Background, Decoration);
}
/// <summary> /// <summary>
/// Converts the string representation of a style to its <see cref="Style"/> equivalent. /// Converts the string representation of a style to its <see cref="Style"/> equivalent.
/// A return value indicates whether the operation succeeded. /// A return value indicates whether the operation succeeded.
@@ -67,7 +76,7 @@ namespace Spectre.Console
/// if the conversion succeeded, or <c>null</c> if the conversion failed. /// if the conversion succeeded, or <c>null</c> if the conversion failed.
/// </param> /// </param>
/// <returns><c>true</c> if s was converted successfully; otherwise, <c>false</c>.</returns> /// <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); return StyleParser.TryParse(text, out result);
} }
@@ -113,13 +122,13 @@ namespace Spectre.Console
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
return Equals(obj as Style); return Equals(obj as Style);
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool Equals(Style other) public bool Equals(Style? other)
{ {
if (other == null) if (other == null)
{ {