From 68292db51f6db54ffca3a6cf617f0462c0524116 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 22 Feb 2026 10:54:20 +0700 Subject: [PATCH 1/5] feat: add Grow property to TableColumn for flexbox-style column sizing Columns with Grow = true expand to fill remaining terminal width after fixed columns are allocated. Multiple grow columns share remaining space evenly. Includes 7 tests covering all edge cases. --- .../timewarp-terminal/widgets/table-column.cs | 12 ++ .../timewarp-terminal/widgets/table-widget.cs | 80 ++++++++ tests/table-widget-07-grow.cs | 171 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 tests/table-widget-07-grow.cs diff --git a/source/timewarp-terminal/widgets/table-column.cs b/source/timewarp-terminal/widgets/table-column.cs index d9da3c5..a3de8fa 100644 --- a/source/timewarp-terminal/widgets/table-column.cs +++ b/source/timewarp-terminal/widgets/table-column.cs @@ -100,4 +100,16 @@ public TableColumn(string header, Alignment alignment) /// /// public TruncateMode TruncateMode { get; set; } = TruncateMode.End; + + /// + /// Gets or sets whether this column should grow to fill remaining terminal width. + /// Multiple growing columns share remaining space evenly. + /// Defaults to false. + /// + /// + /// Growing columns receive space after fixed columns (Grow = false) have been allocated. + /// If the table overflows even after shrinking fixed columns to their MinWidth, + /// growing columns also shrink proportionally. + /// + public bool Grow { get; set; } } diff --git a/source/timewarp-terminal/widgets/table-widget.cs b/source/timewarp-terminal/widgets/table-widget.cs index 22dae66..5bc3b23 100644 --- a/source/timewarp-terminal/widgets/table-widget.cs +++ b/source/timewarp-terminal/widgets/table-widget.cs @@ -193,6 +193,86 @@ private int[] CalculateColumnWidths(int terminalWidth) ? 2 + (ColumnsList.Count - 1) + (2 * ColumnsList.Count) : (ColumnsList.Count - 1) * 2; // Borderless: just column separators + // Separate grow columns from fixed columns + bool hasGrowColumns = ColumnsList.Any(c => c.Grow); + + if (hasGrowColumns) + { + // Allocate fixed columns at natural width, distribute remainder to grow columns + int fixedContentWidth = 0; + for (int i = 0; i < ColumnsList.Count; i++) + { + if (!ColumnsList[i].Grow) + fixedContentWidth += widths[i]; + } + + int availableForGrow = terminalWidth - overhead - fixedContentWidth; + int growCount = ColumnsList.Count(c => c.Grow); + + if (availableForGrow > 0) + { + // Distribute remaining space evenly among grow columns + int perGrowColumn = availableForGrow / growCount; + int remainder = availableForGrow % growCount; + int growIndex = 0; + + for (int i = 0; i < ColumnsList.Count; i++) + { + if (ColumnsList[i].Grow) + { + widths[i] = perGrowColumn + (growIndex < remainder ? 1 : 0); + growIndex++; + } + } + } + else + { + // Not enough space — shrink fixed columns first, then grow columns + int excessWidth = overhead + fixedContentWidth - terminalWidth; + + if (excessWidth > 0 && Shrink) + { + int[] minWidths = new int[ColumnsList.Count]; + for (int i = 0; i < ColumnsList.Count; i++) + minWidths[i] = ColumnsList[i].MinWidth ?? 4; + + // Shrink fixed columns proportionally first + int[] shrinkableAmounts = new int[ColumnsList.Count]; + int totalShrinkable = 0; + for (int i = 0; i < ColumnsList.Count; i++) + { + if (!ColumnsList[i].Grow) + { + shrinkableAmounts[i] = Math.Max(0, widths[i] - minWidths[i]); + totalShrinkable += shrinkableAmounts[i]; + } + } + + int remainingExcess = Math.Min(excessWidth, totalShrinkable); + for (int i = 0; i < ColumnsList.Count; i++) + { + if (!ColumnsList[i].Grow && shrinkableAmounts[i] > 0) + { + int shrinkAmount = (int)Math.Ceiling((double)shrinkableAmounts[i] / totalShrinkable * remainingExcess); + shrinkAmount = Math.Min(shrinkAmount, shrinkableAmounts[i]); + widths[i] -= shrinkAmount; + totalShrinkable -= shrinkableAmounts[i]; + remainingExcess -= shrinkAmount; + } + } + } + + // Grow columns get minimum width (MinWidth or 4) + for (int i = 0; i < ColumnsList.Count; i++) + { + if (ColumnsList[i].Grow) + widths[i] = ColumnsList[i].MinWidth ?? 4; + } + } + + return widths; + } + int contentWidth = widths.Sum(); int totalWidth = overhead + contentWidth; diff --git a/tests/table-widget-07-grow.cs b/tests/table-widget-07-grow.cs new file mode 100644 index 0000000..9c3d371 --- /dev/null +++ b/tests/table-widget-07-grow.cs @@ -0,0 +1,171 @@ +#!/usr/bin/dotnet -- +#:project ../source/timewarp-terminal/timewarp-terminal.csproj + +// Test Table widget Grow column functionality + +#if !JARIBU_MULTI +return await RunAllTests(); +#endif + +namespace TimeWarp.Terminal.Tests.Core.TableWidgetGrow +{ + +[TestTag("Widgets")] +public class TableWidgetGrowTests +{ + [ModuleInitializer] + internal static void Register() => RegisterTests(); + + public static async Task Should_grow_column_to_fill_terminal_width() + { + // Arrange + TableColumn growColumn = new("Description") { Grow = true }; + Table table = new TableBuilder() + .AddColumn("ID") + .AddColumn(growColumn) + .AddRow("1", "Short text") + .Border(BorderStyle.Rounded) + .Build(); + + // Act - render to 80 chars + string[] lines = table.Render(80); + + // Assert - table should fill terminal width + int topLineWidth = AnsiStringUtils.GetVisibleLength(lines[0]); + topLineWidth.ShouldBe(80); + + await Task.CompletedTask; + } + + public static async Task Should_keep_fixed_columns_at_natural_width() + { + // Arrange - ID column is fixed, Description grows + TableColumn growColumn = new("Description") { Grow = true }; + Table table = new TableBuilder() + .AddColumn("ID") + .AddColumn(growColumn) + .AddRow("1", "Some text") + .Border(BorderStyle.Rounded) + .Build(); + + // Act + string[] lines = table.Render(80); + + // Assert - ID column stays at natural width (1 char content), Description fills the rest + // lines[0] = top border, lines[1] = header, lines[2] = header separator, lines[3] = data row + string dataRow = lines[3]; + dataRow.ShouldContain("1"); // ID content present + dataRow.ShouldContain("Some text"); // Description content present + + await Task.CompletedTask; + } + + public static async Task Should_split_remaining_space_evenly_between_multiple_grow_columns() + { + // Arrange - two grow columns share remaining space + TableColumn col1 = new("Left") { Grow = true }; + TableColumn col2 = new("Right") { Grow = true }; + Table table = new TableBuilder() + .AddColumn("ID") + .AddColumn(col1) + .AddColumn(col2) + .AddRow("1", "A", "B") + .Border(BorderStyle.Rounded) + .Build(); + + // Act - render to 80 chars + string[] lines = table.Render(80); + + // Assert - total width fills terminal + int topLineWidth = AnsiStringUtils.GetVisibleLength(lines[0]); + topLineWidth.ShouldBe(80); + + await Task.CompletedTask; + } + + public static async Task Should_shrink_fixed_columns_when_overflow() + { + // Arrange - fixed columns are wide, terminal is narrow + TableColumn growColumn = new("Description") { Grow = true }; + Table table = new TableBuilder() + .AddColumn(new TableColumn("Fixed Header")) + .AddColumn(growColumn) + .AddRow("Fixed content", "Grow content") + .Border(BorderStyle.Rounded) + .Build(); + + // Act - narrow terminal forces shrinking of fixed columns + string[] lines = table.Render(40); + + // Assert - table fits within terminal width + int topLineWidth = AnsiStringUtils.GetVisibleLength(lines[0]); + topLineWidth.ShouldBeLessThanOrEqualTo(40); + + await Task.CompletedTask; + } + + public static async Task Should_give_grow_column_min_width_when_no_space_available() + { + // Arrange - very narrow terminal, no room for grow column + TableColumn growColumn = new("G") { Grow = true }; + Table table = new TableBuilder() + .AddColumn(new TableColumn("Fixed") { MinWidth = 10 }) + .AddColumn(growColumn) + .AddRow("Fixed text", "Grow text") + .Border(BorderStyle.Rounded) + .Build(); + + // Act - very narrow terminal + string[] lines = table.Render(20); + + // Assert - table still renders (grow column falls back to MinWidth ?? 4) + lines.Length.ShouldBeGreaterThan(0); + + await Task.CompletedTask; + } + + public static async Task Should_grow_column_with_three_fixed_columns() + { + // Arrange - real-world: ID, Status, Description (grows), Timestamp + TableColumn descColumn = new("Description") { Grow = true }; + Table table = new TableBuilder() + .AddColumn("ID") + .AddColumn("Status") + .AddColumn(descColumn) + .AddColumn("Time") + .AddRow("1", "Active", "This description will expand to fill available space", "12:00") + .Border(BorderStyle.Rounded) + .Build(); + + // Act + string[] lines = table.Render(100); + + // Assert - table fills terminal + int topLineWidth = AnsiStringUtils.GetVisibleLength(lines[0]); + topLineWidth.ShouldBe(100); + + await Task.CompletedTask; + } + + public static async Task Should_render_grow_column_with_ellipsis_when_fixed_overflow() + { + // Arrange - fixed columns exceed terminal, grow column falls back to MinWidth and truncates + TableColumn growColumn = new("Description") { Grow = true, MinWidth = 4 }; + Table table = new TableBuilder() + .AddColumn(new TableColumn("VeryLongFixedColumnHeader")) + .AddColumn(growColumn) + .AddRow("Very long fixed content that takes a lot of space", "Some description text here") + .Border(BorderStyle.Rounded) + .Build(); + + // Act - narrow terminal + string[] lines = table.Render(30); + + // Assert - still renders without throwing + lines.Length.ShouldBeGreaterThan(0); + + await Task.CompletedTask; + } +} + +} // namespace TimeWarp.Terminal.Tests.Core.TableWidgetGrow From 1af2fb8d6a93158aaa7b9d45c1ea4ac64591c96e Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 22 Feb 2026 11:03:39 +0700 Subject: [PATCH 2/5] samples: add Grow column example showing reusable column definitions --- samples/table-widget.cs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/samples/table-widget.cs b/samples/table-widget.cs index b9200ca..69ac01e 100755 --- a/samples/table-widget.cs +++ b/samples/table-widget.cs @@ -193,5 +193,41 @@ .AddRow("E2E", "FAILED".Red())) .WriteRule() .WriteLine("Done!") + .WriteLine(); + +// Example 14: Grow column (flexbox-style) +terminal + .WriteLine("14. Grow Column (fills remaining terminal width)") + .WriteLine("------------------------------------------------"); + +// Define columns once, reuse with different data +TableColumn[] statusColumns = +[ + new("ID"), + new("Status"), + new TableColumn("Description") { Grow = true } +]; + +terminal + .WriteLine("Short IDs and statuses — Description fills the rest:") + .WriteTable(t => t + .AddColumn(statusColumns[0]) + .AddColumn(statusColumns[1]) + .AddColumn(statusColumns[2]) + .AddRow("1", "Active".Green(), "Short description") + .AddRow("2", "Pending".Yellow(), "Another description that is a bit longer than the first") + .AddRow("3", "Failed".Red(), "Description grows to fill all remaining terminal width") + .Border(BorderStyle.Rounded)) + .WriteLine() + .WriteLine("Wide IDs and long statuses — fixed columns expand, Description shrinks to compensate:") + .WriteTable(t => t + .AddColumn(statusColumns[0]) + .AddColumn(statusColumns[1]) + .AddColumn(statusColumns[2]) + .AddRow("10001", "In Progress".Cyan(), "Short desc") + .AddRow("99999", "Waiting For Review".Yellow(), "Another short desc") + .AddRow("42042", "Deployment Failed".Red(), "And one more") + .Border(BorderStyle.Rounded)) .WriteLine() + .WriteLine("Note: Fixed columns (ID, Status) size to their content; Description fills the rest.") .WriteLine("Demo complete!"); From 529e7e1e5d6b05bf12d763418ab6b0efb8ae8a59 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 22 Feb 2026 11:25:48 +0700 Subject: [PATCH 3/5] chore: Update Directory.Packages.props to remove unused dependencies and update TimeWarp packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused package groups: Microsoft Extensions, Mediator, Serilog, OpenTelemetry, Aspire, Benchmark packages, MCP Server, FluentValidation - Update TimeWarp.Amuru 1.0.0-beta.17 -> 1.0.0-beta.19 - Update TimeWarp.Nuru 3.0.0-beta.47 -> 3.0.0-beta.54 - Update Microsoft.CodeAnalysis.NetAnalyzers 10.0.101 -> 10.0.103 - Remove TimeWarp.Terminal and TimeWarp.OptionsValidation (unused) - Remove TimeWarp.Builder and TimeWarp.Jaribu (likely unused) 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode --- Directory.Packages.props | 64 +++------------------------------------- 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ac17dd2..cb1f9c4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,75 +1,19 @@ + true - - - - - - - - - - - - - - - - - - - + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive @@ -78,7 +22,7 @@ - + From 63133fcdb3749e44e4510d1b948aae081496ddd0 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 22 Feb 2026 11:28:08 +0700 Subject: [PATCH 4/5] chore: bump version to 1.0.0-beta.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode --- source/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 3adf6a2..35fea81 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.0-beta.5 + 1.0.0-beta.6 Steven T. Cramer https://github.com/TimeWarpEngineering/timewarp-terminal Unlicense From fd912c5d3705f59d38f2eca5c26600da24866e3e Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 22 Feb 2026 11:33:53 +0700 Subject: [PATCH 5/5] fix: remove Mediator references from dev-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev-cli was referencing Mediator packages that were removed from the central package management. The Nuru Endpoint DSL doesn't require Mediator - it uses its own source generator. 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode --- tools/dev-cli/Directory.Build.props | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/dev-cli/Directory.Build.props b/tools/dev-cli/Directory.Build.props index 725a9e8..8620838 100644 --- a/tools/dev-cli/Directory.Build.props +++ b/tools/dev-cli/Directory.Build.props @@ -58,11 +58,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive -