diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 97fab628..19531e17 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -1,11 +1,13 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using CreativeCoders.Core; using JetBrains.Annotations; using Spectre.Console; namespace CreativeCoders.SysConsole.Core; -[ExcludeFromCodeCoverage] [PublicAPI] public static class AnsiConsoleExtensions { @@ -21,6 +23,7 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c return new AnsiConsolePrint(ansiConsole); } + [PublicAPI] public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -31,6 +34,7 @@ public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Colo return ansiConsole; } + [PublicAPI] public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -40,4 +44,43 @@ public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, return ansiConsole; } + + [PublicAPI] + public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable items, + TableColumnDef[] columns, Action? configureTable = null) + { + var table = new Table + { + ShowHeaders = false, + Border = TableBorder.None + }; + + foreach (var tableColumnDef in columns) + { + table.AddColumn(tableColumnDef.GetTitle(), x => + { + x.Width = tableColumnDef.Width; + tableColumnDef.ConfigureColumn(x); + }); + + if (!string.IsNullOrWhiteSpace(tableColumnDef.Title)) + { + table.ShowHeaders = true; + } + } + + configureTable?.Invoke(table); + + if (table.ShowHeaders) + { + table.AddRow(columns.Select(x => new string('=', x.Title?.Length ?? 0)).ToArray()); + } + + foreach (var item in items) + { + table.AddRow(columns.Select(x => x.GetValue(item)).ToArray()); + } + + ansiConsole.Write(table); + } } diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs new file mode 100644 index 00000000..f7d27d82 --- /dev/null +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs @@ -0,0 +1,33 @@ +using System; +using CreativeCoders.Core; +using Spectre.Console; + +namespace CreativeCoders.SysConsole.Core; + +public class TableColumnDef( + Func valueSelector, + string? title = null, + int? width = null, + Color? color = null, + Action? configureColumn = null) +{ + private readonly Func _valueSelector = Ensure.NotNull(valueSelector); + + public string GetValue(T item) => + GetStringWithColor(_valueSelector(item)?.ToString() ?? string.Empty); + + public void ConfigureColumn(TableColumn column) => configureColumn?.Invoke(column); + + private string GetStringWithColor(string text) + { + return color == null + ? text + : $"[{color.Value.ToMarkup()}]{text}[/]"; + } + + public string GetTitle() => GetStringWithColor(Title ?? string.Empty); + + public string? Title { get; } = title; + + public int? Width { get; } = width; +} diff --git a/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs new file mode 100644 index 00000000..cfa7d695 --- /dev/null +++ b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs @@ -0,0 +1,251 @@ +using System.Diagnostics.CodeAnalysis; +using AwesomeAssertions; +using CreativeCoders.SysConsole.Core; +using FakeItEasy; +using Spectre.Console; +using Spectre.Console.Rendering; +using Xunit; + +namespace CreativeCoders.SysConsole.UnitTests; + +[SuppressMessage("ReSharper", "NullableWarningSuppressionIsUsed")] +public class AnsiConsoleExtensionsTests +{ + [Fact] + public void PrintTable_ItemsAndColumns_TableIsWrittenToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 }, + new TestItem { Name = "Item2", Value = 20 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name"), + new TableColumnDef(x => x.Value, "Value") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(3); // 1 header separator row + 2 data rows + capturedTable.ShowHeaders.Should().BeTrue(); + } + + [Fact] + public void PrintTable_NoColumnTitles_HeadersAreNotShown() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.ShowHeaders.Should().BeFalse(); + capturedTable.Rows.Count.Should().Be(1); // Only data row + } + + [Fact] + public void PrintTable_WithConfigureTable_TableIsConfigured() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns, table => table.Title = new TableTitle("TestTitle")); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Title!.Text.Should().Be("TestTitle"); + } + + [Fact] + public void PrintTable_WithColumnWidth_ColumnHasSpecifiedWidth() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name", width: 20) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Columns[0].Width.Should().Be(20); + } + + [Fact] + public void PrintTable_EmptyItems_OnlyHeaderRowsAreWritten() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = Enumerable.Empty(); + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(1); // Only header separator row + } + + [Fact] + public void PrintTable_ColoredColumns_TableIsWrittenWithColorToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name", color: Color.Red), + new TableColumnDef(x => x.Value, "Value", color: Color.Green) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(2); + + var row = (capturedTable.Rows as IReadOnlyList)[1]; + + var col0 = row[0].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col0.Text + .Should().Be("Item1"); + + col0.Style.Foreground + .Should().Be(Color.Red); + + var col1 = row[1].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col1.Text + .Should().Be("10"); + + col1.Style.Foreground + .Should().Be(Color.Green); + } + + [Fact] + public void PrintTable_NotColoredColumns_TableIsWrittenWithOutColorToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name"), + new TableColumnDef(x => x.Value, "Value") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(2); + + var row = (capturedTable.Rows as IReadOnlyList)[1]; + + var col0 = row[0].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col0.Text + .Should().Be("Item1"); + + col0.Style.Foreground + .Should().Be(Color.Default); + + var col1 = row[1].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col1.Text + .Should().Be("10"); + + col1.Style.Foreground + .Should().Be(Color.Default); + } + + private class TestItem + { + public string Name { get; init; } = string.Empty; + + public int Value { get; init; } + } +}