Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -21,6 +23,7 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c
return new AnsiConsolePrint(ansiConsole);
}

[PublicAPI]
public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
Color? backgroundColor = null)
{
Expand All @@ -31,6 +34,7 @@ public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Colo
return ansiConsole;
}

[PublicAPI]
public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
Color? backgroundColor = null)
{
Expand All @@ -40,4 +44,43 @@ public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value,

return ansiConsole;
}

[PublicAPI]
public static void PrintTable<T>(this IAnsiConsole ansiConsole, IEnumerable<T> items,
TableColumnDef<T>[] columns, Action<Table>? 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);
}
}
33 changes: 33 additions & 0 deletions source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using CreativeCoders.Core;
using Spectre.Console;

namespace CreativeCoders.SysConsole.Core;

public class TableColumnDef<T>(
Func<T, object?> valueSelector,
string? title = null,
int? width = null,
Color? color = null,
Action<TableColumn>? configureColumn = null)
{
private readonly Func<T, object?> _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;
}
Original file line number Diff line number Diff line change
@@ -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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 },
new TestItem { Name = "Item2", Value = 20 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name"),
new TableColumnDef<TestItem>(x => x.Value, "Value")
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name)
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name")
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name", width: 20)
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(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<IAnsiConsole>();

var items = Enumerable.Empty<TestItem>();

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name")
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name", color: Color.Red),
new TableColumnDef<TestItem>(x => x.Value, "Value", color: Color.Green)
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(0));

// Act
ansiConsole.PrintTable(items, columns);

// Assert
capturedTable.Should().NotBeNull();
capturedTable!.Rows.Count.Should().Be(2);

var row = (capturedTable.Rows as IReadOnlyList<TableRow>)[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<IAnsiConsole>();

var items = new[]
{
new TestItem { Name = "Item1", Value = 10 }
};

var columns = new[]
{
new TableColumnDef<TestItem>(x => x.Name, "Name"),
new TableColumnDef<TestItem>(x => x.Value, "Value")
};

Table? capturedTable = null;
A.CallTo(() => ansiConsole.Write(A<IRenderable>.Ignored))
.Invokes(call => capturedTable = call.Arguments.Get<Table>(0));

// Act
ansiConsole.PrintTable(items, columns);

// Assert
capturedTable.Should().NotBeNull();
capturedTable!.Rows.Count.Should().Be(2);

var row = (capturedTable.Rows as IReadOnlyList<TableRow>)[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; }
}
}
Loading