mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-14 16:02:50 +08:00
Add support for tables
This commit is contained in:
parent
aa34c145b9
commit
a068fc68c3
@ -78,6 +78,22 @@ namespace Sample
|
||||
Text.New("Right adjusted\nRight",
|
||||
foreground: Color.White),
|
||||
fit: true, content: Justify.Right));
|
||||
|
||||
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);
|
||||
|
||||
table = new Table(BorderKind.Ascii);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
36
src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs
Normal file
36
src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Shouldly;
|
||||
using Spectre.Console.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit.Composition
|
||||
{
|
||||
public sealed class BorderTests
|
||||
{
|
||||
public sealed class TheGetBorderMethod
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(BorderKind.Ascii, typeof(AsciiBorder))]
|
||||
[InlineData(BorderKind.Square, typeof(SquareBorder))]
|
||||
public void Should_Return_Correct_Border_For_Specified_Kind(BorderKind kind, Type expected)
|
||||
{
|
||||
// Given, When
|
||||
var result = Border.GetBorder(kind);
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Throw_If_Unknown_Border_Kind_Is_Specified()
|
||||
{
|
||||
// Given, When
|
||||
var result = Record.Exception(() => Border.GetBorder((BorderKind)int.MaxValue));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<InvalidOperationException>();
|
||||
result.Message.ShouldBe("Unknown border kind");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
158
src/Spectre.Console.Tests/Unit/Composition/TableTests.cs
Normal file
158
src/Spectre.Console.Tests/Unit/Composition/TableTests.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit.Composition
|
||||
{
|
||||
public sealed class TableTests
|
||||
{
|
||||
public sealed class TheAddColumnMethod
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Throw_If_Column_Is_Null()
|
||||
{
|
||||
// Given
|
||||
var table = new Table();
|
||||
|
||||
// When
|
||||
var result = Record.Exception(() => table.AddColumn(null));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<ArgumentNullException>()
|
||||
.ParamName.ShouldBe("column");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TheAddColumnsMethod
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Throw_If_Columns_Are_Null()
|
||||
{
|
||||
// Given
|
||||
var table = new Table();
|
||||
|
||||
// When
|
||||
var result = Record.Exception(() => table.AddColumns(null));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<ArgumentNullException>()
|
||||
.ParamName.ShouldBe("columns");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TheAddRowMethod
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Throw_If_Rows_Are_Null()
|
||||
{
|
||||
// Given
|
||||
var table = new Table();
|
||||
|
||||
// When
|
||||
var result = Record.Exception(() => table.AddRow(null));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<ArgumentNullException>()
|
||||
.ParamName.ShouldBe("columns");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Throw_If_Row_Columns_Is_Less_Than_Number_Of_Columns()
|
||||
{
|
||||
// Given
|
||||
var table = new Table();
|
||||
table.AddColumn("Hello");
|
||||
table.AddColumn("World");
|
||||
|
||||
// When
|
||||
var result = Record.Exception(() => table.AddRow("Foo"));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<InvalidOperationException>();
|
||||
result.Message.ShouldBe("The number of row columns are less than the number of table columns.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Throw_If_Row_Columns_Are_Greater_Than_Number_Of_Columns()
|
||||
{
|
||||
// Given
|
||||
var table = new Table();
|
||||
table.AddColumn("Hello");
|
||||
|
||||
// When
|
||||
var result = Record.Exception(() => table.AddRow("Foo", "Bar"));
|
||||
|
||||
// Then
|
||||
result.ShouldBeOfType<InvalidOperationException>();
|
||||
result.Message.ShouldBe("The number of row columns are greater than the number of table columns.");
|
||||
}
|
||||
}
|
||||
|
||||
[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.AddRow("Grault", "Garply", "Fred");
|
||||
|
||||
// When
|
||||
console.Render(table);
|
||||
|
||||
// Then
|
||||
console.Lines[0].ShouldBe("┌────────┬────────┬───────┐");
|
||||
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
|
||||
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
|
||||
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
|
||||
console.Lines[4].ShouldBe("│ Grault │ Garply │ Fred │");
|
||||
console.Lines[5].ShouldBe("└────────┴────────┴───────┘");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Render_Table_With_Specified_Border()
|
||||
{
|
||||
// Given
|
||||
var console = new PlainConsole(width: 80);
|
||||
var table = new Table(BorderKind.Ascii);
|
||||
table.AddColumns("Foo", "Bar", "Baz");
|
||||
table.AddRow("Qux", "Corgi", "Waldo");
|
||||
table.AddRow("Grault", "Garply", "Fred");
|
||||
|
||||
// When
|
||||
console.Render(table);
|
||||
|
||||
// Then
|
||||
console.Lines[0].ShouldBe("+-------------------------+");
|
||||
console.Lines[1].ShouldBe("| Foo | Bar | Baz |");
|
||||
console.Lines[2].ShouldBe("|--------+--------+-------|");
|
||||
console.Lines[3].ShouldBe("| Qux | Corgi | Waldo |");
|
||||
console.Lines[4].ShouldBe("| Grault | Garply | Fred |");
|
||||
console.Lines[5].ShouldBe("+-------------------------+");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Render_Table_With_Multiple_Rows_In_Cell_Correctly()
|
||||
{
|
||||
// Given
|
||||
var console = new PlainConsole(width: 80);
|
||||
var table = new Table();
|
||||
table.AddColumns("Foo", "Bar", "Baz");
|
||||
table.AddRow("Qux\nQuuux", "Corgi", "Waldo");
|
||||
table.AddRow("Grault", "Garply", "Fred");
|
||||
|
||||
// When
|
||||
console.Render(table);
|
||||
|
||||
// Then
|
||||
console.Lines[0].ShouldBe("┌────────┬────────┬───────┐");
|
||||
console.Lines[1].ShouldBe("│ Foo │ Bar │ Baz │");
|
||||
console.Lines[2].ShouldBe("├────────┼────────┼───────┤");
|
||||
console.Lines[3].ShouldBe("│ Qux │ Corgi │ Waldo │");
|
||||
console.Lines[4].ShouldBe("│ Quuux │ │ │");
|
||||
console.Lines[5].ShouldBe("│ Grault │ Garply │ Fred │");
|
||||
console.Lines[6].ShouldBe("└────────┴────────┴───────┘");
|
||||
}
|
||||
}
|
||||
}
|
@ -197,7 +197,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
public void Should_Throw_If_Foreground_Is_Set_Twice()
|
||||
{
|
||||
// Given, When
|
||||
var result = Style.TryParse("green yellow", out var style);
|
||||
var result = Style.TryParse("green yellow", out _);
|
||||
|
||||
// Then
|
||||
result.ShouldBeFalse();
|
||||
@ -207,7 +207,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
public void Should_Throw_If_Background_Is_Set_Twice()
|
||||
{
|
||||
// Given, When
|
||||
var result = Style.TryParse("green on blue yellow", out var style);
|
||||
var result = Style.TryParse("green on blue yellow", out _);
|
||||
|
||||
// Then
|
||||
result.ShouldBeFalse();
|
||||
@ -217,7 +217,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
public void Should_Throw_If_Color_Name_Could_Not_Be_Found()
|
||||
{
|
||||
// Given, When
|
||||
var result = Style.TryParse("bold lol", out var style);
|
||||
var result = Style.TryParse("bold lol", out _);
|
||||
|
||||
// Then
|
||||
result.ShouldBeFalse();
|
||||
@ -227,7 +227,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
public void Should_Throw_If_Background_Color_Name_Could_Not_Be_Found()
|
||||
{
|
||||
// Given, When
|
||||
var result = Style.TryParse("blue on lol", out var style);
|
||||
var result = Style.TryParse("blue on lol", out _);
|
||||
|
||||
// Then
|
||||
result.ShouldBeFalse();
|
||||
|
@ -10,12 +10,14 @@ namespace Spectre.Console
|
||||
{
|
||||
private static readonly Lazy<IAnsiConsole> _console = new Lazy<IAnsiConsole>(() =>
|
||||
{
|
||||
return Create(new AnsiConsoleSettings
|
||||
var console = Create(new AnsiConsoleSettings
|
||||
{
|
||||
Ansi = AnsiSupport.Detect,
|
||||
ColorSystem = ColorSystemSupport.Detect,
|
||||
Out = System.Console.Out,
|
||||
});
|
||||
Created = true;
|
||||
return console;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
@ -28,6 +30,8 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public static Capabilities Capabilities => Console.Capabilities;
|
||||
|
||||
internal static bool Created { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the buffer width of the console.
|
||||
/// </summary>
|
||||
|
@ -16,16 +16,27 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public ColorSystem ColorSystem { get; }
|
||||
|
||||
internal Capabilities(bool supportsAnsi, ColorSystem colorSystem)
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not
|
||||
/// this is a legacy console (cmd.exe).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only relevant when running on Microsoft Windows.
|
||||
/// </remarks>
|
||||
public bool LegacyConsole { get; }
|
||||
|
||||
internal Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole)
|
||||
{
|
||||
SupportsAnsi = supportsAnsi;
|
||||
ColorSystem = colorSystem;
|
||||
LegacyConsole = legacyConsole;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var supportsAnsi = SupportsAnsi ? "Yes" : "No";
|
||||
var legacyConsole = LegacyConsole ? "Legacy" : "Modern";
|
||||
var bits = ColorSystem switch
|
||||
{
|
||||
ColorSystem.NoColors => "1 bit",
|
||||
@ -36,7 +47,7 @@ namespace Spectre.Console
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
return $"ANSI={supportsAnsi}, Colors={ColorSystem} ({bits})";
|
||||
return $"ANSI={supportsAnsi}, Colors={ColorSystem}, Kind={legacyConsole} ({bits})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
82
src/Spectre.Console/Composition/Border.cs
Normal file
82
src/Spectre.Console/Composition/Border.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a border used by tables.
|
||||
/// </summary>
|
||||
public abstract class Border
|
||||
{
|
||||
private readonly Dictionary<BorderPart, char> _lookup;
|
||||
|
||||
private static readonly Dictionary<BorderKind, Border> _borders = new Dictionary<BorderKind, Border>
|
||||
{
|
||||
{ BorderKind.Ascii, new AsciiBorder() },
|
||||
{ BorderKind.Square, new SquareBorder() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Border"/> class.
|
||||
/// </summary>
|
||||
protected Border()
|
||||
{
|
||||
_lookup = Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Border"/> represented by the specified <see cref="BorderKind"/>.
|
||||
/// </summary>
|
||||
/// <param name="kind">The kind of border to get.</param>
|
||||
/// <returns>A <see cref="Border"/> instance representing the specified <see cref="BorderKind"/>.</returns>
|
||||
public static Border GetBorder(BorderKind kind)
|
||||
{
|
||||
if (!_borders.TryGetValue(kind, out var border))
|
||||
{
|
||||
throw new InvalidOperationException("Unknown border kind");
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private Dictionary<BorderPart, char> Initialize()
|
||||
{
|
||||
var lookup = new Dictionary<BorderPart, char>();
|
||||
foreach (BorderPart part in Enum.GetValues(typeof(BorderPart)))
|
||||
{
|
||||
lookup.Add(part, GetBoxPart(part));
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <param name="count">The number of repetitions.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part, int count)
|
||||
{
|
||||
return new string(GetBoxPart(part), count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part)
|
||||
{
|
||||
return _lookup[part].ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the character representing the specified border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get the character representation for.</param>
|
||||
/// <returns>A character representation of the specified border part.</returns>
|
||||
protected abstract char GetBoxPart(BorderPart part);
|
||||
}
|
||||
}
|
18
src/Spectre.Console/Composition/BorderKind.cs
Normal file
18
src/Spectre.Console/Composition/BorderKind.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents different kinds of borders.
|
||||
/// </summary>
|
||||
public enum BorderKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A square border.
|
||||
/// </summary>
|
||||
Square = 0,
|
||||
|
||||
/// <summary>
|
||||
/// An old school ASCII border.
|
||||
/// </summary>
|
||||
Ascii = 1,
|
||||
}
|
||||
}
|
98
src/Spectre.Console/Composition/BorderPart.cs
Normal file
98
src/Spectre.Console/Composition/BorderPart.cs
Normal file
@ -0,0 +1,98 @@
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the different border parts.
|
||||
/// </summary>
|
||||
public enum BorderPart
|
||||
{
|
||||
/// <summary>
|
||||
/// The top left part of a header.
|
||||
/// </summary>
|
||||
HeaderTopLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The top part of a header.
|
||||
/// </summary>
|
||||
HeaderTop,
|
||||
|
||||
/// <summary>
|
||||
/// The top separator part of a header.
|
||||
/// </summary>
|
||||
HeaderTopSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The top right part of a header.
|
||||
/// </summary>
|
||||
HeaderTopRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a header.
|
||||
/// </summary>
|
||||
HeaderLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A header separator.
|
||||
/// </summary>
|
||||
HeaderSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a header.
|
||||
/// </summary>
|
||||
HeaderRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a header.
|
||||
/// </summary>
|
||||
HeaderBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a cell.
|
||||
/// </summary>
|
||||
CellLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A cell separator.
|
||||
/// </summary>
|
||||
CellSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a cell.
|
||||
/// </summary>
|
||||
ColumnRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a footer.
|
||||
/// </summary>
|
||||
FooterBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomRight,
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Composition/Borders/AsciiBorder.cs
Normal file
37
src/Spectre.Console/Composition/Borders/AsciiBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an old school ASCII border.
|
||||
/// </summary>
|
||||
public sealed class AsciiBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override char GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => '+',
|
||||
BorderPart.HeaderTop => '-',
|
||||
BorderPart.HeaderTopSeparator => '-',
|
||||
BorderPart.HeaderTopRight => '+',
|
||||
BorderPart.HeaderLeft => '|',
|
||||
BorderPart.HeaderSeparator => '|',
|
||||
BorderPart.HeaderRight => '|',
|
||||
BorderPart.HeaderBottomLeft => '|',
|
||||
BorderPart.HeaderBottom => '-',
|
||||
BorderPart.HeaderBottomSeparator => '+',
|
||||
BorderPart.HeaderBottomRight => '|',
|
||||
BorderPart.CellLeft => '|',
|
||||
BorderPart.CellSeparator => '|',
|
||||
BorderPart.ColumnRight => '|',
|
||||
BorderPart.FooterBottomLeft => '+',
|
||||
BorderPart.FooterBottom => '-',
|
||||
BorderPart.FooterBottomSeparator => '-',
|
||||
BorderPart.FooterBottomRight => '+',
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Composition/Borders/SquareBorder.cs
Normal file
37
src/Spectre.Console/Composition/Borders/SquareBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a square border.
|
||||
/// </summary>
|
||||
public sealed class SquareBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override char GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => '┌',
|
||||
BorderPart.HeaderTop => '─',
|
||||
BorderPart.HeaderTopSeparator => '┬',
|
||||
BorderPart.HeaderTopRight => '┐',
|
||||
BorderPart.HeaderLeft => '│',
|
||||
BorderPart.HeaderSeparator => '│',
|
||||
BorderPart.HeaderRight => '│',
|
||||
BorderPart.HeaderBottomLeft => '├',
|
||||
BorderPart.HeaderBottom => '─',
|
||||
BorderPart.HeaderBottomSeparator => '┼',
|
||||
BorderPart.HeaderBottomRight => '┤',
|
||||
BorderPart.CellLeft => '│',
|
||||
BorderPart.CellSeparator => '│',
|
||||
BorderPart.ColumnRight => '│',
|
||||
BorderPart.FooterBottomLeft => '└',
|
||||
BorderPart.FooterBottom => '─',
|
||||
BorderPart.FooterBottomSeparator => '┴',
|
||||
BorderPart.FooterBottomRight => '┘',
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
262
src/Spectre.Console/Composition/Table.cs
Normal file
262
src/Spectre.Console/Composition/Table.cs
Normal file
@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Spectre.Console.Composition;
|
||||
using Spectre.Console.Internal;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a table.
|
||||
/// </summary>
|
||||
public sealed class Table : IRenderable
|
||||
{
|
||||
private readonly List<Text> _columns;
|
||||
private readonly List<List<Text>> _rows;
|
||||
private readonly Border _border;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Table"/> class.
|
||||
/// </summary>
|
||||
/// <param name="border">The border to use.</param>
|
||||
public Table(BorderKind border = BorderKind.Square)
|
||||
{
|
||||
_columns = new List<Text>();
|
||||
_rows = new List<List<Text>>();
|
||||
_border = Border.GetBorder(border);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the table.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
public void AddColumn(string column)
|
||||
{
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
_columns.Add(Text.New(column));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple columns to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public void AddColumns(params string[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
_columns.AddRange(columns.Select(column => Text.New(column)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
public void AddRow(params string[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
if (columns.Length < _columns.Count)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are less than the number of table columns.");
|
||||
}
|
||||
|
||||
if (columns.Length > _columns.Count)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Measure(Encoding encoding, int maxWidth)
|
||||
{
|
||||
// Calculate the max width for each column.
|
||||
var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
|
||||
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
|
||||
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
|
||||
{
|
||||
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
|
||||
{
|
||||
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
|
||||
if (columnWidth > columnWidths[columnIndex])
|
||||
{
|
||||
columnWidths[columnIndex] = columnWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We now know the max width of each column, so let's recalculate the width.
|
||||
return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
||||
{
|
||||
// Calculate the max width for each column.
|
||||
var maxColumnWidth = (width - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
|
||||
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
|
||||
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
|
||||
{
|
||||
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
|
||||
{
|
||||
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
|
||||
if (columnWidth > columnWidths[columnIndex])
|
||||
{
|
||||
columnWidths[columnIndex] = columnWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We now know the max width of each column, so let's recalculate the width.
|
||||
width = columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
|
||||
|
||||
// Create the rows.
|
||||
var rows = new List<List<Text>>();
|
||||
rows.Add(new List<Text>(_columns));
|
||||
rows.AddRange(_rows);
|
||||
|
||||
// Iterate all rows.
|
||||
var result = new List<Segment>();
|
||||
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
|
||||
{
|
||||
var cellHeight = 1;
|
||||
|
||||
// Get the list of cells for the row.
|
||||
var cells = new List<List<SegmentLine>>();
|
||||
|
||||
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s)))
|
||||
{
|
||||
var lines = Segment.SplitLines(cell.Render(encoding, rowWidth));
|
||||
cellHeight = Math.Max(cellHeight, lines.Count);
|
||||
cells.Add(lines);
|
||||
}
|
||||
|
||||
if (firstRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft)));
|
||||
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
// Iterate through each cell row
|
||||
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
|
||||
{
|
||||
// Make cells the same shape
|
||||
MakeSameHeight(cellHeight, cells);
|
||||
|
||||
var w00t = cells.Enumerate().ToArray();
|
||||
foreach (var (cellIndex, firstCell, lastCell, cell) in w00t)
|
||||
{
|
||||
if (firstCell)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.CellLeft)));
|
||||
}
|
||||
|
||||
result.Add(new Segment(" "));
|
||||
result.AddRange(cell[cellRowIndex]);
|
||||
|
||||
// Pad cell right
|
||||
var length = cell[cellRowIndex].Sum(segment => segment.CellLength(encoding));
|
||||
if (length < columnWidths[cellIndex])
|
||||
{
|
||||
result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
|
||||
}
|
||||
|
||||
result.Add(new Segment(" "));
|
||||
|
||||
if (lastCell)
|
||||
{
|
||||
// Separator
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.ColumnRight)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Separator
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.CellSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
if (firstRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
if (lastRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cell.Count < cellHeight)
|
||||
{
|
||||
while (cell.Count != cellHeight)
|
||||
{
|
||||
cell.Add(new SegmentLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -12,6 +13,7 @@ namespace Spectre.Console
|
||||
/// 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;
|
||||
@ -98,15 +100,24 @@ namespace Spectre.Console
|
||||
/// <inheritdoc/>
|
||||
public int Measure(Encoding encoding, int maxWidth)
|
||||
{
|
||||
var lines = _text.SplitLines();
|
||||
return lines.Max(x => x.CellLength(encoding));
|
||||
var lines = Segment.SplitLines(Render(encoding, maxWidth));
|
||||
if (lines.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return lines.Max(x => x.Length);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
||||
{
|
||||
var result = new List<Segment>();
|
||||
if (string.IsNullOrWhiteSpace(_text))
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var result = new List<Segment>();
|
||||
var segments = SplitLineBreaks(CreateSegments());
|
||||
|
||||
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
|
||||
|
@ -32,12 +32,12 @@ namespace Spectre.Console.Internal
|
||||
new Regex("bvterm"), // Bitvise SSH Client
|
||||
};
|
||||
|
||||
public static bool Detect(bool upgrade)
|
||||
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool upgrade)
|
||||
{
|
||||
// Github action doesn't setup a correct PTY but supports ANSI.
|
||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_ACTION")))
|
||||
{
|
||||
return true;
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
// Running on Windows?
|
||||
@ -47,10 +47,11 @@ namespace Spectre.Console.Internal
|
||||
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
|
||||
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
return Windows.SupportsAnsi(upgrade);
|
||||
var supportsAnsi = Windows.SupportsAnsi(upgrade, out var legacyConsole);
|
||||
return (supportsAnsi, legacyConsole);
|
||||
}
|
||||
|
||||
// Check if the terminal is of type ANSI/VT100/xterm compatible.
|
||||
@ -59,11 +60,11 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
if (_regexes.Any(regex => regex.IsMatch(term)))
|
||||
{
|
||||
return true;
|
||||
return (true, false);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1060:Move pinvokes to native methods class")]
|
||||
@ -91,8 +92,10 @@ namespace Spectre.Console.Internal
|
||||
public static extern uint GetLastError();
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||
public static bool SupportsAnsi(bool upgrade)
|
||||
public static bool SupportsAnsi(bool upgrade, out bool isLegacy)
|
||||
{
|
||||
isLegacy = false;
|
||||
|
||||
try
|
||||
{
|
||||
var @out = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
@ -104,6 +107,8 @@ namespace Spectre.Console.Internal
|
||||
|
||||
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
|
||||
{
|
||||
isLegacy = true;
|
||||
|
||||
if (!upgrade)
|
||||
{
|
||||
return false;
|
||||
|
@ -41,12 +41,12 @@ namespace Spectre.Console.Internal
|
||||
}
|
||||
}
|
||||
|
||||
public AnsiConsoleRenderer(TextWriter @out, ColorSystem system)
|
||||
public AnsiConsoleRenderer(TextWriter @out, ColorSystem system, bool legacyConsole)
|
||||
{
|
||||
_out = @out ?? throw new ArgumentNullException(nameof(@out));
|
||||
_system = system;
|
||||
|
||||
Capabilities = new Capabilities(true, system);
|
||||
Capabilities = new Capabilities(true, system, legacyConsole);
|
||||
Encoding = @out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8;
|
||||
Foreground = Color.Default;
|
||||
Background = Color.Default;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
@ -13,9 +14,41 @@ namespace Spectre.Console.Internal
|
||||
|
||||
var buffer = settings.Out ?? System.Console.Out;
|
||||
|
||||
var supportsAnsi = settings.Ansi == AnsiSupport.Detect
|
||||
? AnsiDetector.Detect(true)
|
||||
: settings.Ansi == AnsiSupport.Yes;
|
||||
var supportsAnsi = settings.Ansi == AnsiSupport.Yes;
|
||||
var legacyConsole = false;
|
||||
|
||||
if (settings.Ansi == AnsiSupport.Detect)
|
||||
{
|
||||
(supportsAnsi, legacyConsole) = AnsiDetector.Detect(true);
|
||||
|
||||
// Check whether or not this is a legacy console from the existing instance (if any).
|
||||
// We need to do this because once we upgrade the console to support ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
// on Windows, there is no way of detecting whether or not we're running on a legacy console or not.
|
||||
if (AnsiConsole.Created && !legacyConsole && buffer.IsStandardOut() && AnsiConsole.Capabilities.LegacyConsole)
|
||||
{
|
||||
legacyConsole = AnsiConsole.Capabilities.LegacyConsole;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.IsStandardOut())
|
||||
{
|
||||
// Are we running on Windows?
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Not the first console we're creating?
|
||||
if (AnsiConsole.Created)
|
||||
{
|
||||
legacyConsole = AnsiConsole.Capabilities.LegacyConsole;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try detecting whether or not this
|
||||
(_, legacyConsole) = AnsiDetector.Detect(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect
|
||||
? ColorSystemDetector.Detect(supportsAnsi)
|
||||
@ -23,13 +56,13 @@ namespace Spectre.Console.Internal
|
||||
|
||||
if (supportsAnsi)
|
||||
{
|
||||
return new AnsiConsoleRenderer(buffer, colorSystem)
|
||||
return new AnsiConsoleRenderer(buffer, colorSystem, legacyConsole)
|
||||
{
|
||||
Decoration = Decoration.None,
|
||||
};
|
||||
}
|
||||
|
||||
return new FallbackConsoleRenderer(buffer, colorSystem);
|
||||
return new FallbackConsoleRenderer(buffer, colorSystem, legacyConsole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ namespace Spectre.Console.Internal
|
||||
}
|
||||
}
|
||||
|
||||
public FallbackConsoleRenderer(TextWriter @out, ColorSystem system)
|
||||
public FallbackConsoleRenderer(TextWriter @out, ColorSystem system, bool legacyConsole)
|
||||
{
|
||||
_out = @out;
|
||||
_system = system;
|
||||
@ -105,7 +105,7 @@ namespace Spectre.Console.Internal
|
||||
Encoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
Capabilities = new Capabilities(false, _system);
|
||||
Capabilities = new Capabilities(false, _system, legacyConsole);
|
||||
}
|
||||
|
||||
public void Write(string text)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
@ -35,7 +36,7 @@ namespace Spectre.Console.Internal
|
||||
else if (token.Kind == MarkupTokenKind.Text)
|
||||
{
|
||||
// Get the effecive style.
|
||||
var effectiveStyle = style.Combine(stack);
|
||||
var effectiveStyle = style.Combine(stack.Reverse());
|
||||
result.Append(token.Value, effectiveStyle);
|
||||
}
|
||||
else
|
||||
|
@ -18,12 +18,7 @@ namespace Spectre.Console.Internal
|
||||
public static bool TryParse(string text, out Style style)
|
||||
{
|
||||
style = Parse(text, out var error);
|
||||
if (error != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return error == null;
|
||||
}
|
||||
|
||||
private static Style Parse(string text, out string error)
|
||||
|
Loading…
x
Reference in New Issue
Block a user