fix(cli): single-key note rows render as raw indented lines under their step (Task 14 review)

This commit is contained in:
Joseph Doherty
2026-09-09 12:15:05 -04:00
parent c6529fa996
commit c1b1702397
5 changed files with 159 additions and 17 deletions
@@ -31,19 +31,74 @@ public static class OutputFormatter
/// <param name="rows">Data rows; each inner array corresponds to a column in the same order as <paramref name="headers"/>.</param>
/// <param name="headers">Column header labels.</param>
public static void WriteTable(IEnumerable<string[]> rows, string[] headers)
=> WriteTable(rows.Select(TableRow.Columns), headers);
/// <summary>
/// One row for the <see cref="TableRow"/> overload of <c>WriteTable</c>: either padded
/// column cells like the plain <see cref="string[]"/> overload, or an already-formatted
/// line printed verbatim with no column padding/alignment at all (Task 14 review,
/// 2026-09-09) — used for a note/log line folded onto a step, which must render as a raw
/// indented line under that step's row rather than as a cell in a table column (every
/// other row's cell there would otherwise render blank, and the line's own leading
/// whitespace would be swallowed by column padding).
/// </summary>
internal readonly struct TableRow
{
var allRows = new List<string[]> { headers };
allRows.AddRange(rows);
private TableRow(string[]? cells, string? rawLine)
{
Cells = cells;
RawLine = rawLine;
}
internal string[]? Cells { get; }
internal string? RawLine { get; }
/// <summary>A normal row, rendered as padded columns aligned with the header set.</summary>
internal static TableRow Columns(string[] cells) => new(cells, null);
/// <summary>An already-formatted line, printed exactly as given with no column alignment.</summary>
internal static TableRow Raw(string line) => new(null, line);
}
/// <summary>
/// Writes a plain-text padded table like <see cref="WriteTable(IEnumerable{string[]}, string[])"/>,
/// except a <see cref="TableRow.Raw"/> row is printed verbatim — unpadded, uninvolved in the
/// column-width computation — at its position in the row sequence, immediately after
/// whatever row precedes it.
/// </summary>
/// <param name="rows">Data rows, each either padded columns or a raw line.</param>
/// <param name="headers">Column header labels.</param>
internal static void WriteTable(IEnumerable<TableRow> rows, string[] headers)
{
var rowList = rows.ToList();
var widths = new int[headers.Length];
foreach (var row in allRows)
void Consider(string[] row)
{
for (int i = 0; i < Math.Min(row.Length, widths.Length); i++)
widths[i] = Math.Max(widths[i], (row[i] ?? "").Length);
}
foreach (var row in allRows)
Consider(headers);
foreach (var row in rowList)
if (row.Cells != null)
Consider(row.Cells);
void PrintColumns(string[] row)
{
for (int i = 0; i < headers.Length; i++)
Console.Write((i < row.Length ? row[i] ?? "" : "").PadRight(widths[i] + 2));
Console.WriteLine();
}
PrintColumns(headers);
foreach (var row in rowList)
{
if (row.RawLine != null)
Console.WriteLine(row.RawLine);
else
PrintColumns(row.Cells!);
}
}
}