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
57 changes: 57 additions & 0 deletions ChangeLogs/2.2.0-ChangeLog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# V2.2.0

## Multi-line support
Cell content can contain newlines and the table will adjust row height accordingly, in headers, rows and footers.

## Bug fix
- Edge case drawing issues of cell borders miss alligned when having empty rows or empty cell content fixed
- Performance improvements


```csharp
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog snippet uses Environment.NewLine and List<string[]>, but only imports ConsoleTable.Text. Consider adding using System; and using System.Collections.Generic; so the example compiles without relying on implicit global usings.

Suggested change
```csharp
```csharp
using System;
using System.Collections.Generic;

Copilot uses AI. Check for mistakes.
using ConsoleTable.Text;

// Setup the table
var table = new Table
{
RowTextAlignmentRight = false,
HeaderTextAlignmentRight = false,
FooterTextAlignmentRight = false,
Padding = 2,
Headers = new string[] { "Name", $"Age{Environment.NewLine}&{Environment.NewLine}Birthyear", "City" },
Rows = new List<string[]>
{
new string[] { "Alice Cooper", $"30{Environment.NewLine}1995", "New York" },
new string[] { "Bob", $"25{Environment.NewLine}2000", "Los Angeles" },
new string[] { "Charlie Brown", $"67{Environment.NewLine}1958", "Chicago" },
new string[] { "Gloria", $"40{Environment.NewLine}1985", $"Chicago{Environment.NewLine}Originally from Bogota, Colombia" }
},
Comment on lines +26 to +28
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the example string: "Originally fron Bogota, Colombia" should be "Originally from Bogota, Colombia".

Copilot uses AI. Check for mistakes.
Footers = new string[] { $"Total: 4{Environment.NewLine}3 Male - 1 Female", "Total Age: 162" }
};

// Display the table
Console.WriteLine(table.ToTable());
```

Output:
```
┌─────────────────────┬──────────────────┬────────────────────────────────────┐
│ Name │ Age │ City │
│ │ & │ │
│ │ Birthyear │ │
├═════════════════════┼══════════════════┼════════════════════════════════════┤
│ Alice Cooper │ 30 │ New York │
│ │ 1995 │ │
├─────────────────────┼──────────────────┼────────────────────────────────────┤
│ Bob │ 25 │ Los Angeles │
│ │ 2000 │ │
├─────────────────────┼──────────────────┼────────────────────────────────────┤
│ Charlie Brown │ 67 │ Chicago │
│ │ 1958 │ │
├─────────────────────┼──────────────────┼────────────────────────────────────┤
│ Gloria │ 40 │ Chicago │
│ │ 1985 │ Originally from Bogota, Colombia │
└─────────────────────┴──────────────────┴────────────────────────────────────┘
Total: 4 Total Age: 162
3 Male - 1 Female
```
52 changes: 47 additions & 5 deletions ConsoleTable.Text.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ static void Main(string[] args)

WriteTableFluent();

WriteMultiLineTable(false);

WriteMultiLineTable(true);

WriteTableWithoutBorders();

//WriteBigTable();
Expand Down Expand Up @@ -74,6 +78,35 @@ private static void WriteDefaultTable()
Console.WriteLine();
}

private static void WriteMultiLineTable(bool textAlignRight)
{
Console.WriteLine();
Console.WriteLine("Multi line table:");
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor wording: consider changing the output label to “Multi-line table:” for consistency with the README/changelog phrasing.

Suggested change
Console.WriteLine("Multi line table:");
Console.WriteLine("Multi-line table:");

Copilot uses AI. Check for mistakes.
Console.WriteLine("Text align " + (textAlignRight ? "right" : "left"));

// Setup the table
var table = new Table
{
RowTextAlignmentRight = textAlignRight,
HeaderTextAlignmentRight = textAlignRight,
FooterTextAlignmentRight = textAlignRight,
Padding = 2,
Headers = new string[] { "Name", $"Age{Environment.NewLine}&{Environment.NewLine}Birthyear", "City" },
Rows = new List<string[]>
{
new string[] { "Alice Cooper", $"30{Environment.NewLine}1995", "New York" },
new string[] { "Bob", $"25{Environment.NewLine}2000", "Los Angeles" },
new string[] { "Charlie Brown", $"67{Environment.NewLine}1958", "Chicago" },
new string[] { "Gloria", $"40{Environment.NewLine}1985", $"Chicago{Environment.NewLine}Originally from Bogota, Colombia" }
},
Footers = new string[] { $"Total: 4{Environment.NewLine}3 Male - 1 Female", "Total Age: 162" }
};

// Display the table
Console.WriteLine(table.ToTable());
Console.WriteLine();
}

private static void WriteDefaultTableWithProperties()
{
Console.WriteLine();
Expand Down Expand Up @@ -187,12 +220,12 @@ private static void WriteTableEachRowRandom()
table.AddRow("Bob", "25", "Antwerp", "Belgium");
table.AddRow("Charlie", "47", "Chicago");
table.AddRow("Karina", "33", "Lima", "Peru", "South-America");
table.AddRow("Jenny", "43");
table.AddRow("Jenny", $"43{Environment.NewLine}1982");
table.AddRow("John");
table.AddRow("Johny");
table.AddRow();
table.AddRow(null!);
table.AddRow("Thomas", "33", "Brussels", "Belgium", "Europe", "Earth", "Solar System");
table.AddRow("Thomas", "33", "Brussels", $"Belgium{Environment.NewLine}BE", "Europe", "Earth", "Solar System");
table.AddRow("Nathalie", "29", "Paris", "France", "Europe", "Earth", "Solar System");
table.AddRow("Mathias", "37", "Oslo", "Norway", "Europe", "Earth", "Solar System");
table.AddRow("Kenny", "55", "Tokyo");
Expand Down Expand Up @@ -303,7 +336,10 @@ private static void WriteBigTable()
var headers = new List<string>();
for (var columnPos = 1; columnPos <= columnCount; columnPos++)
{
headers.Add($"Header {columnPos}");
if (columnPos % 2 == 0)
headers.Add($"Header {columnPos}");
else
headers.Add($"MultiLine{Environment.NewLine}Header {columnPos}");
}
table.Headers = headers.ToArray();

Expand All @@ -313,7 +349,10 @@ private static void WriteBigTable()
var row = new string[columnCount];
for (var columnPos = 1; columnPos <= columnCount; columnPos++)
{
row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}";
if (columnPos % 2 == 0)
row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}";
else
row[columnPos - 1] = $"Row {rowPos}{Environment.NewLine}Column {columnPos}";
}
rows.Add(row);
}
Expand All @@ -322,7 +361,10 @@ private static void WriteBigTable()
var footers = new List<string>();
for (var columnPos = 1; columnPos <= columnCount; columnPos++)
{
footers.Add($"Footer {columnPos}");
if (columnPos % 2 == 0)
footers.Add($"Footer {columnPos}");
else
footers.Add($"Footer{Environment.NewLine}{columnPos}");
}
table.Footers = footers.ToArray();

Expand Down
2 changes: 1 addition & 1 deletion ConsoleTable.Text/ConsoleTable.Text.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<!-- NuGet Package Metadata -->
<PackageId>ConsoleTable.Text</PackageId>
<Version>2.1.0</Version>
<Version>2.2.0</Version>
<Authors>Bruno Van Thournout</Authors>
<Description>A library for creating a formatted string table with customizable headers, footers, rows and easy to use styling options.</Description>
<PackageProjectUrl>https://github.com/BrunoVT1992/ConsoleTable</PackageProjectUrl>
Expand Down
125 changes: 81 additions & 44 deletions ConsoleTable.Text/Table.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -245,35 +245,37 @@ public string ToTable()
int[] maximumCellWidths = GetMaxCellWidths();

var topLineCreated = false;
var previousRow = Array.Empty<string>();
var nextRow = Array.Empty<string>();
var previousRowColumnCount = 0;
var nextRowColumnCount = 0;

if (Headers?.Any() == true)
{
var headerColumnCount = GetColumnCount(Headers);

if (ShowBorders)
{
formattedTable = CreateTopLine(maximumCellWidths, Headers.Count(), formattedTable);
formattedTable = CreateTopLine(maximumCellWidths, headerColumnCount, formattedTable);
topLineCreated = true;
}

formattedTable = CreateValueLine(maximumCellWidths, Headers, HeaderTextAlignmentRight, ShowBorders ? TableDrawing.VerticalLine : TableDrawing.EmptySpace, formattedTable);

previousRow = Headers;
previousRowColumnCount = headerColumnCount;

//When there are no rows immediatly draw the bottom line after the header
//When there are no rows immediately draw the bottom line after the header
if (Rows?.Any() == true)
{
nextRow = Rows.First();
nextRowColumnCount = GetColumnCount(Rows.First());
if (ShowBorders)
{
formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalHeaderLine, formattedTable);
formattedTable = CreateSeperatorLine(maximumCellWidths, previousRowColumnCount, nextRowColumnCount, TableDrawing.HorizontalHeaderLine, formattedTable);
}
}
else
{
if (ShowBorders)
{
formattedTable = CreateBottomLine(maximumCellWidths, Headers.Count(), TableDrawing.HorizontalHeaderLine, formattedTable);
formattedTable = CreateBottomLine(maximumCellWidths, headerColumnCount, TableDrawing.HorizontalHeaderLine, formattedTable);
}
}
}
Expand All @@ -282,7 +284,7 @@ public string ToTable()
{
if (!topLineCreated && ShowBorders)
{
formattedTable = CreateTopLine(maximumCellWidths, Rows.First().Count(), formattedTable);
formattedTable = CreateTopLine(maximumCellWidths, GetColumnCount(Rows.First()), formattedTable);
topLineCreated = true;
}

Expand All @@ -291,19 +293,19 @@ public string ToTable()

for (int i = 0; i < Rows.Count; i++)
{
var row = CleanupRow(Rows[i]);
var row = Rows[i];

formattedTable = CreateValueLine(maximumCellWidths, row, RowTextAlignmentRight, ShowBorders ? TableDrawing.VerticalLine : TableDrawing.EmptySpace, formattedTable);

previousRow = row;
previousRowColumnCount = GetColumnCount(row);

if (rowIndex != lastRowIndex)
{
nextRow = CleanupRow(Rows[rowIndex + 1]);
nextRowColumnCount = GetColumnCount(Rows[rowIndex + 1]);

if (ShowBorders)
{
formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalLine, formattedTable);
formattedTable = CreateSeperatorLine(maximumCellWidths, previousRowColumnCount, nextRowColumnCount, TableDrawing.HorizontalLine, formattedTable);
}
}

Expand All @@ -312,7 +314,7 @@ public string ToTable()

if (ShowBorders)
{
formattedTable = CreateBottomLine(maximumCellWidths, previousRow.Count(), TableDrawing.HorizontalLine, formattedTable);
formattedTable = CreateBottomLine(maximumCellWidths, previousRowColumnCount, TableDrawing.HorizontalLine, formattedTable);
}
}

Expand All @@ -334,13 +336,20 @@ public override string ToString()
return ToTable();
}

private static string[] CleanupRow(string[] row)
private static int GetColumnCount(string[] row)
{
//Weird behaviour with empty rows. So we create 1 column with a space inside.
if (row == null || row.Length <= 0)
return new string[] { " " };
return 1;

return row.Length;
}

return row;
private static string[] GetCellLines(string cellValue)
{
if (string.IsNullOrEmpty(cellValue))
return new string[] { string.Empty };

return cellValue.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
}

private int[] GetMaxCellWidths()
Expand All @@ -365,8 +374,9 @@ private int[] GetMaxCellWidths()
var maximumColumns = 0;
foreach (var row in table)
{
if (row != null && row.Length > maximumColumns)
maximumColumns = row.Length;
var colCount = GetColumnCount(row);
if (colCount > maximumColumns)
maximumColumns = colCount;
}

var maximumCellWidths = new int[maximumColumns];
Expand All @@ -385,13 +395,28 @@ private int[] GetMaxCellWidths()

for (int i = 0; i < row.Length; i++)
{
var maxWidth = row[i].Length + paddingCount;
var lines = GetCellLines(row[i]);
var maxLineWidth = 0;
for (int l = 0; l < lines.Length; l++)
{
if (lines[l].Length > maxLineWidth)
maxLineWidth = lines[l].Length;
}

var maxWidth = maxLineWidth + paddingCount;

if (maxWidth > maximumCellWidths[i])
maximumCellWidths[i] = maxWidth;
}
}

//Ensure every column has at least the padding width
for (int i = 0; i < maximumCellWidths.Length; i++)
{
if (maximumCellWidths[i] < paddingCount)
maximumCellWidths[i] = paddingCount;
}

return maximumCellWidths;
}

Expand Down Expand Up @@ -431,39 +456,51 @@ private StringBuilder CreateBottomLine(int[] maximumCellWidths, int rowColumnCou

private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, bool alignRight, string verticalLine, StringBuilder formattedTable)
{
int cellIndex = 0;
int lastCellIndex = row.Length - 1;
int columnCount = GetColumnCount(row);

var cellLines = new string[columnCount][];
int maxLineCount = 1;
for (int i = 0; i < columnCount; i++)
{
var cellValue = (row != null && i < row.Length) ? row[i] : null;
cellLines[i] = GetCellLines(cellValue);
if (cellLines[i].Length > maxLineCount)
maxLineCount = cellLines[i].Length;
}

int lastCellIndex = columnCount - 1;

var paddingString = string.Empty;
if (Padding > 0)
paddingString = string.Concat(Enumerable.Repeat(' ', Padding));

for (int i = 0; i < row.Length; i++)
for (int lineIndex = 0; lineIndex < maxLineCount; lineIndex++)
{
var column = row[i];

var leftVerticalLine = verticalLine;
if (i == 0 && !ShowBorders)
for (int i = 0; i < columnCount; i++)
{
leftVerticalLine = TableDrawing.Empty;
}
var column = lineIndex < cellLines[i].Length ? cellLines[i][lineIndex] : string.Empty;

var restWidth = maximumCellWidths[cellIndex];
if (Padding > 0)
restWidth -= Padding * 2;
var leftVerticalLine = verticalLine;
if (i == 0 && !ShowBorders)
{
leftVerticalLine = TableDrawing.Empty;
}

var cellValue = alignRight ? column.PadLeft(restWidth, ' ') : column.PadRight(restWidth, ' ');
var restWidth = maximumCellWidths[i];
if (Padding > 0)
restWidth -= Padding * 2;

if (cellIndex == 0 && cellIndex == lastCellIndex)
formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine));
else if (cellIndex == 0)
formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString));
else if (cellIndex == lastCellIndex)
formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine));
else
formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString));
var cellValue = alignRight ? column.PadLeft(restWidth, ' ') : column.PadRight(restWidth, ' ');

cellIndex++;
if (i == 0 && i == lastCellIndex)
formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine));
else if (i == 0)
formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString));
else if (i == lastCellIndex)
formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine));
else
formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString));
}
}

return formattedTable;
Expand Down
Loading
Loading