diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.v3.ncrunchproject b/src/Spectre.Console.Tests/Spectre.Console.Tests.v3.ncrunchproject
new file mode 100644
index 0000000..2bc3e06
--- /dev/null
+++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.v3.ncrunchproject
@@ -0,0 +1,18 @@
+
+
+
+
+ Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small
+
+
+ Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small
+
+
+ Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small
+
+
+ Spectre.Console.Tests.Unit.Cli.CommandAppTests+Parsing+UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small
+
+
+
+
\ No newline at end of file
diff --git a/src/Spectre.Console/Extensions/EnumerableExtensions.cs b/src/Spectre.Console/Extensions/EnumerableExtensions.cs
index 3172af7..0ee405d 100644
--- a/src/Spectre.Console/Extensions/EnumerableExtensions.cs
+++ b/src/Spectre.Console/Extensions/EnumerableExtensions.cs
@@ -6,6 +6,23 @@ namespace Spectre.Console.Internal
{
internal static class EnumerableExtensions
{
+ public static int IndexOf(this IEnumerable source, T item)
+ where T : class
+ {
+ var index = 0;
+ foreach (var candidate in source)
+ {
+ if (candidate == item)
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return -1;
+ }
+
public static int GetCount(this IEnumerable source)
{
if (source is IList list)
diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs
deleted file mode 100644
index e98f61e..0000000
--- a/src/Spectre.Console/Widgets/Table.cs
+++ /dev/null
@@ -1,501 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Spectre.Console.Internal;
-using Spectre.Console.Rendering;
-
-namespace Spectre.Console
-{
- ///
- /// A renderable table.
- ///
- public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
- {
- private const int EdgeCount = 2;
-
- private readonly List _columns;
- private readonly List _rows;
-
- private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
- private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
-
- ///
- /// Gets the table columns.
- ///
- public IReadOnlyList Columns => _columns;
-
- ///
- /// Gets the table rows.
- ///
- public IReadOnlyList Rows => _rows;
-
- ///
- public TableBorder Border { get; set; } = TableBorder.Square;
-
- ///
- public Style? BorderStyle { get; set; }
-
- ///
- public bool UseSafeBorder { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether or not table headers should be shown.
- ///
- public bool ShowHeaders { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether or not table footers should be shown.
- ///
- public bool ShowFooters { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether or not the table should
- /// fit the available space. If false, the table width will be
- /// auto calculated. Defaults to false.
- ///
- public bool Expand { get; set; }
-
- ///
- /// Gets or sets the width of the table.
- ///
- public int? Width { get; set; }
-
- ///
- /// Gets or sets the table title.
- ///
- public TableTitle? Title { get; set; }
-
- ///
- /// Gets or sets the table footnote.
- ///
- public TableTitle? Caption { get; set; }
-
- ///
- public Justify? Alignment { get; set; }
-
- // 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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public Table()
- {
- _columns = new List();
- _rows = new List();
- }
-
- ///
- /// Adds a column to the table.
- ///
- /// The column to add.
- /// The same instance so that multiple calls can be chained.
- public Table AddColumn(TableColumn column)
- {
- if (column is null)
- {
- throw new ArgumentNullException(nameof(column));
- }
-
- if (_rows.Count > 0)
- {
- throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
- }
-
- _columns.Add(column);
- return this;
- }
-
- ///
- /// Adds a row to the table.
- ///
- /// The row columns to add.
- /// The same instance so that multiple calls can be chained.
- public Table AddRow(IEnumerable columns)
- {
- if (columns is null)
- {
- throw new ArgumentNullException(nameof(columns));
- }
-
- var rowColumnCount = columns.GetCount();
- if (rowColumnCount > _columns.Count)
- {
- throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
- }
-
- _rows.Add(new TableRow(columns));
-
- // Need to add missing columns?
- if (rowColumnCount < _columns.Count)
- {
- var diff = _columns.Count - rowColumnCount;
- Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
- }
-
- return this;
- }
-
- ///
- protected override Measurement Measure(RenderContext context, int maxWidth)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (Width != null)
- {
- maxWidth = Math.Min(Width.Value, maxWidth);
- }
-
- maxWidth -= GetExtraWidth(includePadding: true);
-
- var measurements = _columns.Select(column => MeasureColumn(column, context, maxWidth)).ToList();
- var min = measurements.Sum(x => x.Min) + GetExtraWidth(includePadding: true);
- var max = Width ?? measurements.Sum(x => x.Max) + GetExtraWidth(includePadding: true);
-
- return new Measurement(min, max);
- }
-
- ///
- protected override IEnumerable Render(RenderContext context, int maxWidth)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- var border = Border.GetSafeBorder((context.LegacyConsole || !context.Unicode) && UseSafeBorder);
- var borderStyle = BorderStyle ?? Style.Plain;
-
- var tableWidth = maxWidth;
- var actualMaxWidth = maxWidth;
-
- var showBorder = Border.Visible;
- var hideBorder = !Border.Visible;
- var hasRows = _rows.Count > 0;
- var hasFooters = _columns.Any(c => c.Footer != null);
-
- if (Width != null)
- {
- maxWidth = Math.Min(Width.Value, maxWidth);
- }
-
- maxWidth -= GetExtraWidth(includePadding: true);
-
- // Calculate the column and table widths
- var columnWidths = CalculateColumnWidths(context, maxWidth);
-
- // Update the table width.
- tableWidth = columnWidths.Sum() + GetExtraWidth(includePadding: true);
- if (tableWidth <= 0 || tableWidth > actualMaxWidth || columnWidths.Any(c => c <= 0))
- {
- return new List(new[] { new Segment("…", BorderStyle ?? Style.Plain) });
- }
-
- var rows = new List();
- if (ShowHeaders)
- {
- // Add columns to top of rows
- rows.Add(new TableRow(new List(_columns.Select(c => c.Header))));
- }
-
- // Add rows.
- rows.AddRange(_rows);
-
- if (hasFooters)
- {
- rows.Add(new TableRow(new List(_columns.Select(c => c.Footer ?? Text.Empty))));
- }
-
- var result = new List();
- result.AddRange(RenderAnnotation(context, Title, actualMaxWidth, tableWidth, _defaultHeadingStyle));
-
- // Iterate all rows
- foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
- {
- var cellHeight = 1;
-
- // Get the list of cells for the row and calculate the cell height
- var cells = new List>();
- foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
- {
- var justification = _columns[columnIndex].Alignment;
- var childContext = context.WithJustification(justification);
-
- var lines = Segment.SplitLines(context, cell.Render(childContext, rowWidth));
- cellHeight = Math.Max(cellHeight, lines.Count);
- cells.Add(lines);
- }
-
- // Show top of header?
- if (firstRow && showBorder)
- {
- var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Top, columnWidths, _columns), Alignment, actualMaxWidth);
- result.Add(new Segment(separator, borderStyle));
- result.Add(Segment.LineBreak);
- }
-
- // Show footer separator?
- if (ShowFooters && lastRow && showBorder && hasFooters)
- {
- var textBorder = border.GetColumnRow(TablePart.FooterSeparator, columnWidths, _columns);
- if (!string.IsNullOrEmpty(textBorder))
- {
- var separator = Aligner.Align(context, textBorder, Alignment, actualMaxWidth);
- result.Add(new Segment(separator, borderStyle));
- result.Add(Segment.LineBreak);
- }
- }
-
- // Make cells the same shape
- cells = Segment.MakeSameHeight(cellHeight, cells);
-
- // Iterate through each cell row
- foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
- {
- var rowResult = new List();
-
- foreach (var (cellIndex, firstCell, lastCell, cell) in cells.Enumerate())
- {
- if (firstCell && showBorder)
- {
- // Show left column edge
- var part = firstRow && ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
- rowResult.Add(new Segment(border.GetPart(part), borderStyle));
- }
-
- // Pad column on left side.
- if (showBorder || IsGrid)
- {
- var leftPadding = _columns[cellIndex].Padding.GetLeftSafe();
- if (leftPadding > 0)
- {
- rowResult.Add(new Segment(new string(' ', leftPadding)));
- }
- }
-
- // Add content
- rowResult.AddRange(cell[cellRowIndex]);
-
- // Pad cell content right
- var length = cell[cellRowIndex].Sum(segment => segment.CellCount(context));
- if (length < columnWidths[cellIndex])
- {
- rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
- }
-
- // Pad column on the right side
- if (showBorder || (hideBorder && !lastCell) || (hideBorder && lastCell && IsGrid && PadRightCell))
- {
- var rightPadding = _columns[cellIndex].Padding.GetRightSafe();
- if (rightPadding > 0)
- {
- rowResult.Add(new Segment(new string(' ', rightPadding)));
- }
- }
-
- if (lastCell && showBorder)
- {
- // Add right column edge
- var part = firstRow && ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
- rowResult.Add(new Segment(border.GetPart(part), borderStyle));
- }
- else if (showBorder)
- {
- // Add column separator
- var part = firstRow && ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
- rowResult.Add(new Segment(border.GetPart(part), borderStyle));
- }
- }
-
- // Align the row result.
- Aligner.Align(context, rowResult, Alignment, actualMaxWidth);
-
- // Is the row larger than the allowed max width?
- if (Segment.CellCount(context, rowResult) > actualMaxWidth)
- {
- result.AddRange(Segment.Truncate(context, rowResult, actualMaxWidth));
- }
- else
- {
- result.AddRange(rowResult);
- }
-
- result.Add(Segment.LineBreak);
- }
-
- // Show header separator?
- if (firstRow && showBorder && ShowHeaders && hasRows)
- {
- var separator = Aligner.Align(context, border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, _columns), Alignment, actualMaxWidth);
- result.Add(new Segment(separator, borderStyle));
- result.Add(Segment.LineBreak);
- }
-
- // Show bottom of footer?
- if (lastRow && showBorder)
- {
- var separator = Aligner.Align(context, border.GetColumnRow(TablePart.Bottom, columnWidths, _columns), Alignment, actualMaxWidth);
- result.Add(new Segment(separator, borderStyle));
- result.Add(Segment.LineBreak);
- }
- }
-
- result.AddRange(RenderAnnotation(context, Caption, actualMaxWidth, tableWidth, _defaultCaptionStyle));
-
- return result;
- }
-
- // Calculate the widths of each column, including padding, not including borders.
- // Ported from Rich by Will McGugan, licensed under MIT.
- // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
- private List CalculateColumnWidths(RenderContext options, int maxWidth)
- {
- var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth)).ToArray();
- var widths = width_ranges.Select(range => range.Max).ToList();
-
- var tableWidth = widths.Sum();
- if (tableWidth > maxWidth)
- {
- var wrappable = _columns.Select(c => !c.NoWrap).ToList();
- widths = CollapseWidths(widths, wrappable, maxWidth);
- tableWidth = widths.Sum();
-
- // last resort, reduce columns evenly
- if (tableWidth > maxWidth)
- {
- var excessWidth = tableWidth - maxWidth;
- widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
- tableWidth = widths.Sum();
- }
- }
-
- if (tableWidth < maxWidth && ShouldExpand())
- {
- var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
- widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
- }
-
- return widths;
- }
-
- // Reduce widths so that the total is less or equal to the max width.
- // Ported from Rich by Will McGugan, licensed under MIT.
- // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442
- private static List CollapseWidths(List widths, List wrappable, int maxWidth)
- {
- var totalWidth = widths.Sum();
- var excessWidth = totalWidth - maxWidth;
-
- if (wrappable.AnyTrue())
- {
- while (totalWidth != 0 && excessWidth > 0)
- {
- var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
- .Where(x => x.allowWrap)
- .Max(x => x.width);
-
- var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
- var columnDifference = maxColumn - secondMaxColumn;
-
- var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
- if (!ratios.Any(x => x != 0) || columnDifference == 0)
- {
- break;
- }
-
- var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList();
- widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths);
-
- totalWidth = widths.Sum();
- excessWidth = totalWidth - maxWidth;
- }
- }
-
- return widths;
- }
-
- private IEnumerable RenderAnnotation(
- RenderContext context, TableTitle? header,
- int maxWidth, int tableWidth, Style defaultStyle)
- {
- if (header == null)
- {
- return Array.Empty();
- }
-
- var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
- .Alignment(Justify.Center)
- .Overflow(Overflow.Ellipsis);
-
- var items = new List();
- items.AddRange(((IRenderable)paragraph).Render(context, tableWidth));
-
- // Align over the whole buffer area
- Aligner.Align(context, items, Alignment, maxWidth);
-
- items.Add(Segment.LineBreak);
- return items;
- }
-
- private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
- {
- // Predetermined width?
- if (column.Width != null)
- {
- return (column.Width.Value, column.Width.Value);
- }
-
- var columnIndex = _columns.IndexOf(column);
- var rows = _rows.Select(row => row[columnIndex]);
-
- var minWidths = new List();
- var maxWidths = new List();
-
- // Include columns (both header and footer) in measurement
- var headerMeasure = column.Header.Measure(options, maxWidth);
- var footerMeasure = column.Footer?.Measure(options, maxWidth) ?? headerMeasure;
- minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
- maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
-
- foreach (var row in rows)
- {
- var rowMeasure = row.Measure(options, maxWidth);
- minWidths.Add(rowMeasure.Min);
- maxWidths.Add(rowMeasure.Max);
- }
-
- var padding = column.Padding?.GetWidth() ?? 0;
-
- return (minWidths.Count > 0 ? minWidths.Max() : padding,
- maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
- }
-
- private int GetExtraWidth(bool includePadding)
- {
- var hideBorder = !Border.Visible;
- var separators = hideBorder ? 0 : _columns.Count - 1;
- var edges = hideBorder ? 0 : EdgeCount;
- var padding = includePadding ? _columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum() : 0;
-
- if (!PadRightCell)
- {
- padding -= _columns.Last().Padding.GetRightSafe();
- }
-
- return separators + edges + padding;
- }
-
- private bool ShouldExpand()
- {
- return Expand || Width != null;
- }
- }
-}
diff --git a/src/Spectre.Console/Widgets/Table/Table.cs b/src/Spectre.Console/Widgets/Table/Table.cs
new file mode 100644
index 0000000..f0ff0da
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Table/Table.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A renderable table.
+ ///
+ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
+ {
+ private readonly List _columns;
+ private readonly List _rows;
+
+ ///
+ /// Gets the table columns.
+ ///
+ public IReadOnlyList Columns => _columns;
+
+ ///
+ /// Gets the table rows.
+ ///
+ public IReadOnlyList Rows => _rows;
+
+ ///
+ public TableBorder Border { get; set; } = TableBorder.Square;
+
+ ///
+ public Style? BorderStyle { get; set; }
+
+ ///
+ public bool UseSafeBorder { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether or not table headers should be shown.
+ ///
+ public bool ShowHeaders { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether or not table footers should be shown.
+ ///
+ public bool ShowFooters { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether or not the table should
+ /// fit the available space. If false, the table width will be
+ /// auto calculated. Defaults to false.
+ ///
+ public bool Expand { get; set; }
+
+ ///
+ /// Gets or sets the width of the table.
+ ///
+ public int? Width { get; set; }
+
+ ///
+ /// Gets or sets the table title.
+ ///
+ public TableTitle? Title { get; set; }
+
+ ///
+ /// Gets or sets the table footnote.
+ ///
+ public TableTitle? Caption { get; set; }
+
+ ///
+ public Justify? Alignment { get; set; }
+
+ // 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;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Table()
+ {
+ _columns = new List();
+ _rows = new List();
+ }
+
+ ///
+ /// Adds a column to the table.
+ ///
+ /// The column to add.
+ /// The same instance so that multiple calls can be chained.
+ public Table AddColumn(TableColumn column)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (_rows.Count > 0)
+ {
+ throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
+ }
+
+ _columns.Add(column);
+ return this;
+ }
+
+ ///
+ /// Adds a row to the table.
+ ///
+ /// The row columns to add.
+ /// The same instance so that multiple calls can be chained.
+ public Table AddRow(IEnumerable columns)
+ {
+ if (columns is null)
+ {
+ throw new ArgumentNullException(nameof(columns));
+ }
+
+ var rowColumnCount = columns.GetCount();
+ if (rowColumnCount > _columns.Count)
+ {
+ throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
+ }
+
+ _rows.Add(new TableRow(columns));
+
+ // Need to add missing columns?
+ if (rowColumnCount < _columns.Count)
+ {
+ var diff = _columns.Count - rowColumnCount;
+ Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
+ }
+
+ return this;
+ }
+
+ ///
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var measurer = new TableMeasurer(this, context);
+
+ // Calculate the total cell width
+ var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
+
+ // Calculate the minimum and maximum table width
+ var measurements = _columns.Select(column => measurer.MeasureColumn(column, totalCellWidth));
+ var minTableWidth = measurements.Sum(x => x.Min) + measurer.GetNonColumnWidth();
+ var maxTableWidth = Width ?? measurements.Sum(x => x.Max) + measurer.GetNonColumnWidth();
+ return new Measurement(minTableWidth, maxTableWidth);
+ }
+
+ ///
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var measurer = new TableMeasurer(this, context);
+
+ // Calculate the column and table width
+ var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
+ var columnWidths = measurer.CalculateColumnWidths(totalCellWidth);
+ var tableWidth = columnWidths.Sum() + measurer.GetNonColumnWidth();
+
+ // Get the rows to render
+ var rows = GetRenderableRows();
+
+ // Render the table
+ return TableRenderer.Render(
+ new TableRendererContext(this, context, rows, tableWidth, maxWidth),
+ columnWidths);
+ }
+
+ private List GetRenderableRows()
+ {
+ var rows = new List();
+
+ // Show headers?
+ if (ShowHeaders)
+ {
+ rows.Add(TableRow.Header(_columns.Select(c => c.Header)));
+ }
+
+ // Add rows
+ rows.AddRange(_rows);
+
+ // Show footers?
+ if (ShowFooters && _columns.Any(c => c.Footer != null))
+ {
+ rows.Add(TableRow.Footer(_columns.Select(c => c.Footer ?? Text.Empty)));
+ }
+
+ return rows;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/Table/TableAccessor.cs b/src/Spectre.Console/Widgets/Table/TableAccessor.cs
new file mode 100644
index 0000000..ded3dd7
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Table/TableAccessor.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal abstract class TableAccessor
+ {
+ private readonly Table _table;
+
+ public RenderContext Options { get; }
+ public IReadOnlyList Columns => _table.Columns;
+ public virtual IReadOnlyList Rows => _table.Rows;
+ public bool Expand => _table.Expand || _table.Width != null;
+
+ protected TableAccessor(Table table, RenderContext options)
+ {
+ _table = table ?? throw new ArgumentNullException(nameof(table));
+ Options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/TableColumn.cs b/src/Spectre.Console/Widgets/Table/TableColumn.cs
similarity index 100%
rename from src/Spectre.Console/Widgets/TableColumn.cs
rename to src/Spectre.Console/Widgets/Table/TableColumn.cs
diff --git a/src/Spectre.Console/Widgets/Table/TableMeasurer.cs b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs
new file mode 100644
index 0000000..fa474aa
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal sealed class TableMeasurer : TableAccessor
+ {
+ private const int EdgeCount = 2;
+
+ private readonly int? _explicitWidth;
+ private readonly TableBorder _border;
+ private readonly bool _padRightCell;
+
+ public TableMeasurer(Table table, RenderContext options)
+ : base(table, options)
+ {
+ _explicitWidth = table.Width;
+ _border = table.Border;
+ _padRightCell = table.PadRightCell;
+ }
+
+ public int CalculateTotalCellWidth(int maxWidth)
+ {
+ var totalCellWidth = maxWidth;
+ if (_explicitWidth != null)
+ {
+ totalCellWidth = Math.Min(_explicitWidth.Value, maxWidth);
+ }
+
+ return totalCellWidth - GetNonColumnWidth();
+ }
+
+ ///
+ /// Gets the width of everything that's not a cell.
+ /// That means separators, edges and padding.
+ ///
+ /// The width of everything that's not a cell.
+ public int GetNonColumnWidth()
+ {
+ var hideBorder = !_border.Visible;
+ var separators = hideBorder ? 0 : Columns.Count - 1;
+ var edges = hideBorder ? 0 : EdgeCount;
+ var padding = Columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum();
+
+ if (!_padRightCell)
+ {
+ padding -= Columns.Last().Padding.GetRightSafe();
+ }
+
+ return separators + edges + padding;
+ }
+
+ ///
+ /// Calculates the width of all columns minus any padding.
+ ///
+ /// The maximum width that the columns may occupy.
+ /// A list of column widths.
+ public List CalculateColumnWidths(int maxWidth)
+ {
+ var width_ranges = Columns.Select(column => MeasureColumn(column, maxWidth)).ToArray();
+ var widths = width_ranges.Select(range => range.Max).ToList();
+
+ var tableWidth = widths.Sum();
+ if (tableWidth > maxWidth)
+ {
+ var wrappable = Columns.Select(c => !c.NoWrap).ToList();
+ widths = CollapseWidths(widths, wrappable, maxWidth);
+ tableWidth = widths.Sum();
+
+ // last resort, reduce columns evenly
+ if (tableWidth > maxWidth)
+ {
+ var excessWidth = tableWidth - maxWidth;
+ widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
+ tableWidth = widths.Sum();
+ }
+ }
+
+ if (tableWidth < maxWidth && Expand)
+ {
+ var padWidths = Ratio.Distribute(maxWidth - tableWidth, widths);
+ widths = widths.Zip(padWidths, (a, b) => (a, b)).Select(f => f.a + f.b).ToList();
+ }
+
+ return widths;
+ }
+
+ public Measurement MeasureColumn(TableColumn column, int maxWidth)
+ {
+ // Predetermined width?
+ if (column.Width != null)
+ {
+ return new Measurement(column.Width.Value, column.Width.Value);
+ }
+
+ var columnIndex = Columns.IndexOf(column);
+ var rows = Rows.Select(row => row[columnIndex]);
+
+ var minWidths = new List();
+ var maxWidths = new List();
+
+ // Include columns (both header and footer) in measurement
+ var headerMeasure = column.Header.Measure(Options, maxWidth);
+ var footerMeasure = column.Footer?.Measure(Options, maxWidth) ?? headerMeasure;
+ minWidths.Add(Math.Min(headerMeasure.Min, footerMeasure.Min));
+ maxWidths.Add(Math.Max(headerMeasure.Max, footerMeasure.Max));
+
+ foreach (var row in rows)
+ {
+ var rowMeasure = row.Measure(Options, maxWidth);
+ minWidths.Add(rowMeasure.Min);
+ maxWidths.Add(rowMeasure.Max);
+ }
+
+ var padding = column.Padding?.GetWidth() ?? 0;
+
+ return new Measurement(
+ minWidths.Count > 0 ? minWidths.Max() : padding,
+ maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
+ }
+
+ // Reduce widths so that the total is less or equal to the max width.
+ // Ported from Rich by Will McGugan, licensed under MIT.
+ // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L442
+ private static List CollapseWidths(List widths, List wrappable, int maxWidth)
+ {
+ var totalWidth = widths.Sum();
+ var excessWidth = totalWidth - maxWidth;
+
+ if (wrappable.AnyTrue())
+ {
+ while (totalWidth != 0 && excessWidth > 0)
+ {
+ var maxColumn = widths.Zip(wrappable, (first, second) => (width: first, allowWrap: second))
+ .Where(x => x.allowWrap)
+ .Max(x => x.width);
+
+ var secondMaxColumn = widths.Zip(wrappable, (width, allowWrap) => allowWrap && width != maxColumn ? width : 1).Max();
+ var columnDifference = maxColumn - secondMaxColumn;
+
+ var ratios = widths.Zip(wrappable, (width, allowWrap) => width == maxColumn && allowWrap ? 1 : 0).ToList();
+ if (!ratios.Any(x => x != 0) || columnDifference == 0)
+ {
+ break;
+ }
+
+ var maxReduce = widths.Select(_ => Math.Min(excessWidth, columnDifference)).ToList();
+ widths = Ratio.Reduce(excessWidth, ratios, maxReduce, widths);
+
+ totalWidth = widths.Sum();
+ excessWidth = totalWidth - maxWidth;
+ }
+ }
+
+ return widths;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
new file mode 100644
index 0000000..e82b571
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal static class TableRenderer
+ {
+ private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
+ private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
+
+ public static List Render(TableRendererContext context, List columnWidths)
+ {
+ // Can't render the table?
+ if (context.TableWidth <= 0 || context.TableWidth > context.MaxWidth || columnWidths.Any(c => c <= 0))
+ {
+ return new List(new[] { new Segment("…", context.BorderStyle ?? Style.Plain) });
+ }
+
+ var result = new List();
+ result.AddRange(RenderAnnotation(context, context.Title, _defaultHeadingStyle));
+
+ // Iterate all rows
+ foreach (var (index, isFirstRow, isLastRow, row) in context.Rows.Enumerate())
+ {
+ var cellHeight = 1;
+
+ // Get the list of cells for the row and calculate the cell height
+ var cells = new List>();
+ foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
+ {
+ var justification = context.Columns[columnIndex].Alignment;
+ var childContext = context.Options.WithJustification(justification);
+
+ var lines = Segment.SplitLines(context.Options, cell.Render(childContext, rowWidth));
+ cellHeight = Math.Max(cellHeight, lines.Count);
+ cells.Add(lines);
+ }
+
+ // Show top of header?
+ if (isFirstRow && context.ShowBorder)
+ {
+ var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.Top, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
+ result.Add(new Segment(separator, context.BorderStyle));
+ result.Add(Segment.LineBreak);
+ }
+
+ // Show footer separator?
+ if (context.ShowFooters && isLastRow && context.ShowBorder && context.HasFooters)
+ {
+ var textBorder = context.Border.GetColumnRow(TablePart.FooterSeparator, columnWidths, context.Columns);
+ if (!string.IsNullOrEmpty(textBorder))
+ {
+ var separator = Aligner.Align(context.Options, textBorder, context.Alignment, context.MaxWidth);
+ result.Add(new Segment(separator, context.BorderStyle));
+ result.Add(Segment.LineBreak);
+ }
+ }
+
+ // Make cells the same shape
+ cells = Segment.MakeSameHeight(cellHeight, cells);
+
+ // Iterate through each cell row
+ foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
+ {
+ var rowResult = new List();
+
+ foreach (var (cellIndex, isFirstCell, isLastCell, cell) in cells.Enumerate())
+ {
+ if (isFirstCell && context.ShowBorder)
+ {
+ // Show left column edge
+ var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderLeft : TableBorderPart.CellLeft;
+ rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
+ }
+
+ // Pad column on left side.
+ if (context.ShowBorder || context.IsGrid)
+ {
+ var leftPadding = context.Columns[cellIndex].Padding.GetLeftSafe();
+ if (leftPadding > 0)
+ {
+ rowResult.Add(new Segment(new string(' ', leftPadding)));
+ }
+ }
+
+ // Add content
+ rowResult.AddRange(cell[cellRowIndex]);
+
+ // Pad cell content right
+ var length = cell[cellRowIndex].Sum(segment => segment.CellCount(context.Options));
+ if (length < columnWidths[cellIndex])
+ {
+ rowResult.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
+ }
+
+ // Pad column on the right side
+ if (context.ShowBorder || (context.HideBorder && !isLastCell) || (context.HideBorder && isLastCell && context.IsGrid && context.PadRightCell))
+ {
+ var rightPadding = context.Columns[cellIndex].Padding.GetRightSafe();
+ if (rightPadding > 0)
+ {
+ rowResult.Add(new Segment(new string(' ', rightPadding)));
+ }
+ }
+
+ if (isLastCell && context.ShowBorder)
+ {
+ // Add right column edge
+ var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderRight : TableBorderPart.CellRight;
+ rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
+ }
+ else if (context.ShowBorder)
+ {
+ // Add column separator
+ var part = isFirstRow && context.ShowHeaders ? TableBorderPart.HeaderSeparator : TableBorderPart.CellSeparator;
+ rowResult.Add(new Segment(context.Border.GetPart(part), context.BorderStyle));
+ }
+ }
+
+ // Align the row result.
+ Aligner.Align(context.Options, rowResult, context.Alignment, context.MaxWidth);
+
+ // Is the row larger than the allowed max width?
+ if (Segment.CellCount(context.Options, rowResult) > context.MaxWidth)
+ {
+ result.AddRange(Segment.Truncate(context.Options, rowResult, context.MaxWidth));
+ }
+ else
+ {
+ result.AddRange(rowResult);
+ }
+
+ result.Add(Segment.LineBreak);
+ }
+
+ // Show header separator?
+ if (isFirstRow && context.ShowBorder && context.ShowHeaders && context.HasRows)
+ {
+ var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.HeaderSeparator, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
+ result.Add(new Segment(separator, context.BorderStyle));
+ result.Add(Segment.LineBreak);
+ }
+
+ // Show bottom of footer?
+ if (isLastRow && context.ShowBorder)
+ {
+ var separator = Aligner.Align(context.Options, context.Border.GetColumnRow(TablePart.Bottom, columnWidths, context.Columns), context.Alignment, context.MaxWidth);
+ result.Add(new Segment(separator, context.BorderStyle));
+ result.Add(Segment.LineBreak);
+ }
+ }
+
+ result.AddRange(RenderAnnotation(context, context.Caption, _defaultCaptionStyle));
+ return result;
+ }
+
+ private static IEnumerable RenderAnnotation(TableRendererContext context, TableTitle? header, Style defaultStyle)
+ {
+ if (header == null)
+ {
+ return Array.Empty();
+ }
+
+ var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle)
+ .Alignment(Justify.Center)
+ .Overflow(Overflow.Ellipsis);
+
+ // Render the paragraphs
+ var segments = new List();
+ segments.AddRange(((IRenderable)paragraph).Render(context.Options, context.TableWidth));
+
+ // Align over the whole buffer area
+ Aligner.Align(context.Options, segments, context.Alignment, context.MaxWidth);
+
+ segments.Add(Segment.LineBreak);
+ return segments;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs
new file mode 100644
index 0000000..0ba486d
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal sealed class TableRendererContext : TableAccessor
+ {
+ private readonly Table _table;
+ private readonly List _rows;
+
+ public override IReadOnlyList Rows => _rows;
+
+ public TableBorder Border { get; }
+ public Style BorderStyle { get; }
+ public bool ShowBorder { get; }
+ public bool HasRows { get; }
+ public bool HasFooters { get; }
+
+ ///
+ /// Gets the max width of the destination area.
+ /// The table might take up less than this.
+ ///
+ public int MaxWidth { get; }
+
+ ///
+ /// Gets the width of the table.
+ ///
+ public int TableWidth { get; }
+
+ public bool HideBorder => !ShowBorder;
+ public bool ShowHeaders => _table.ShowHeaders;
+ public bool ShowFooters => _table.ShowFooters;
+ public bool IsGrid => _table.IsGrid;
+ public bool PadRightCell => _table.PadRightCell;
+ public TableTitle? Title => _table.Title;
+ public TableTitle? Caption => _table.Caption;
+ public Justify? Alignment => _table.Alignment;
+
+ public TableRendererContext(Table table, RenderContext options, IEnumerable rows, int tableWidth, int maxWidth)
+ : base(table, options)
+ {
+ _table = table ?? throw new ArgumentNullException(nameof(table));
+ _rows = new List(rows ?? Enumerable.Empty());
+
+ ShowBorder = _table.Border.Visible;
+ HasRows = Rows.Any(row => !row.IsHeader && !row.IsFooter);
+ HasFooters = Rows.Any(column => column.IsFooter);
+ Border = table.Border.GetSafeBorder((options.LegacyConsole || !options.Unicode) && table.UseSafeBorder);
+ BorderStyle = table.BorderStyle ?? Style.Plain;
+
+ TableWidth = tableWidth;
+ MaxWidth = maxWidth;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/TableRow.cs b/src/Spectre.Console/Widgets/Table/TableRow.cs
similarity index 71%
rename from src/Spectre.Console/Widgets/TableRow.cs
rename to src/Spectre.Console/Widgets/Table/TableRow.cs
index 2ad6466..7eeb356 100644
--- a/src/Spectre.Console/Widgets/TableRow.cs
+++ b/src/Spectre.Console/Widgets/Table/TableRow.cs
@@ -12,6 +12,9 @@ namespace Spectre.Console
{
private readonly List _items;
+ internal bool IsHeader { get; }
+ internal bool IsFooter { get; }
+
///
/// Gets a row item at the specified table column index.
///
@@ -27,8 +30,26 @@ namespace Spectre.Console
///
/// The row items.
public TableRow(IEnumerable items)
+ : this(items, false, false)
+ {
+ }
+
+ private TableRow(IEnumerable items, bool isHeader, bool isFooter)
{
_items = new List(items ?? Array.Empty());
+
+ IsHeader = isHeader;
+ IsFooter = isFooter;
+ }
+
+ internal static TableRow Header(IEnumerable items)
+ {
+ return new TableRow(items, true, false);
+ }
+
+ internal static TableRow Footer(IEnumerable items)
+ {
+ return new TableRow(items, false, true);
}
internal void Add(IRenderable item)
diff --git a/src/Spectre.Console/Widgets/TableTitle.cs b/src/Spectre.Console/Widgets/Table/TableTitle.cs
similarity index 100%
rename from src/Spectre.Console/Widgets/TableTitle.cs
rename to src/Spectre.Console/Widgets/Table/TableTitle.cs