Skip to content

Commit 5dfeb23

Browse files
Merge pull request #8 from TimeWarpEngineering/Cramer/2025-12-22/dev
feat: fluent chaining, builder enforcement, NuruConsole rename
2 parents ff1c1fe + def609a commit 5dfeb23

35 files changed

Lines changed: 920 additions & 1079 deletions

README.md

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ using TimeWarp.Terminal;
1717
Terminal.WriteLine("Hello, World!".Green());
1818
Terminal.WriteLine("Warning!".Yellow().Bold());
1919

20-
// Or get a terminal instance
20+
// Or get a terminal instance — all Write methods return ITerminal for fluent chaining
2121
ITerminal terminal = TimeWarpTerminal.Default;
22-
terminal.WritePanel("Important message", "Notice");
23-
terminal.WriteRule("Section");
24-
terminal.WriteTable(t => t
22+
terminal
23+
.WritePanel("Important message", "Notice")
24+
.WriteRule("Section")
25+
.WriteTable(t => t
2526
.AddColumn("Name")
2627
.AddColumn("Value")
27-
.AddRow("Status", "OK".Green()));
28+
.AddRow("Status", "OK".Green()))
29+
.WriteLine("Done!");
2830
```
2931

3032
## Static Terminal API
@@ -74,10 +76,10 @@ Basic console I/O abstraction for testable console applications.
7476
```csharp
7577
public interface IConsole
7678
{
77-
void Write(string message);
78-
void WriteLine(string? message = null);
79+
IConsole Write(string message);
80+
IConsole WriteLine(string? message = null);
7981
Task WriteLineAsync(string? message = null);
80-
void WriteErrorLine(string? message = null);
82+
IConsole WriteErrorLine(string? message = null);
8183
Task WriteErrorLineAsync(string? message = null);
8284
string? ReadLine();
8385
}
@@ -90,6 +92,9 @@ Extended terminal interface with cursor control, colors, and hyperlinks.
9092
```csharp
9193
public interface ITerminal : IConsole
9294
{
95+
new ITerminal Write(string message);
96+
new ITerminal WriteLine(string? message = null);
97+
new ITerminal WriteErrorLine(string? message = null);
9398
ConsoleKeyInfo ReadKey(bool intercept);
9499
void SetCursorPosition(int left, int top);
95100
(int Left, int Top) GetCursorPosition();
@@ -106,7 +111,7 @@ public interface ITerminal : IConsole
106111
| Class | Description |
107112
|-------|-------------|
108113
| `TimeWarpTerminal` | Production `ITerminal` with full terminal capabilities |
109-
| `NuruConsole` | Production `IConsole` wrapping `System.Console` |
114+
| `TimeWarpConsole` | Production `IConsole` wrapping `System.Console` |
110115
| `TestTerminal` | Test implementation with captured output and scripted input |
111116
| `TestConsole` | Simpler test implementation for basic I/O testing |
112117

@@ -145,14 +150,18 @@ terminal.WritePanel("This is important information");
145150
terminal.WritePanel("Content here", "Notice");
146151

147152
// Fluent builder with full options
148-
terminal.WritePanel(panel => panel
153+
terminal.WritePanel
154+
(
155+
panel =>
156+
panel
149157
.Header("Configuration".Cyan().Bold())
150158
.Content("Setting: value")
151159
.Border(BorderStyle.Rounded)
152160
.BorderColor(AnsiColors.Cyan)
153161
.Padding(2, 1)
154162
.Width(60)
155-
.WordWrap(true));
163+
.WordWrap(true)
164+
);
156165
```
157166

158167
**Border Styles:** `Rounded`, `Square`, `Doubled`, `Heavy`, `None`
@@ -163,25 +172,25 @@ Formatted table with columns, alignment, and styling.
163172

164173
```csharp
165174
// Simple table
166-
terminal.WriteTable(t => t
175+
terminal.WriteTable
176+
(
177+
t => t
167178
.AddColumn("Name")
168179
.AddColumn("Value", Alignment.Right)
169180
.AddRow("CPU", "45%")
170-
.AddRow("Memory", "2.1 GB"));
181+
.AddRow("Memory", "2.1 GB")
182+
);
171183

172184
// Full-featured table
173-
Table table = new Table()
185+
terminal.WriteTable(t => t
174186
.AddColumn("Package")
175187
.AddColumn("Downloads", Alignment.Right)
176188
.AddColumn(new TableColumn("Path") { TruncateMode = TruncateMode.Start })
177-
.AddRow("GuardClauses", "12M", "/home/user/packages/guard");
178-
179-
table.Border = BorderStyle.Rounded;
180-
table.BorderColor = AnsiColors.Cyan;
181-
table.Expand = true; // Fill terminal width
182-
table.Shrink = true; // Shrink to fit (default)
183-
table.ShowHeaders = true;
184-
table.ShowRowSeparators = false;
189+
.AddRow("GuardClauses", "12M", "/home/user/packages/guard")
190+
.Border(BorderStyle.Rounded)
191+
.BorderColor(AnsiColors.Cyan)
192+
.Expand()
193+
.Shrink());
185194

186195
terminal.WriteTable(table);
187196
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Make widget constructors internal to enforce builder pattern
2+
3+
## Description
4+
5+
Make Table, Panel, and Rule constructors internal so users must go through TableBuilder, PanelBuilder, and RuleBuilder. This eliminates the broken-chain DX where users start fluent chaining with `new Table().AddColumn().AddRow()` then have to break out to property setters for Border, ShowHeaders, BorderColor, etc. The builders already have the full fluent API — this just removes the bad path.
6+
7+
Follows the same pattern as Nuru's `EndpointBuilder` where `.Done()` returns to the parent and the chain never breaks.
8+
9+
## Checklist
10+
11+
- [x] Make Table constructor internal
12+
- [x] Make Panel constructor internal
13+
- [x] Make Rule constructor internal
14+
- [x] Update all samples to use builder pattern (`Action<XxxBuilder>` or `new XxxBuilder()`)
15+
- [x] Update tests to use builder pattern
16+
- [x] Update README.md examples
17+
- [x] Update XML doc comments in source files
18+
- [x] Delete NestedXxxBuilder types (dead code — zero references)
19+
- [x] Make Table fluent methods (AddColumn, AddColumns, AddRow) internal to prevent post-build mutation
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Make terminal Write methods return ITerminal for fluent chaining
2+
3+
## Description
4+
5+
Change Write/WriteLine/WriteTable/WritePanel/WriteRule to return `ITerminal` (or `IConsole`) instead of `void`, enabling fluent chaining:
6+
7+
```csharp
8+
terminal
9+
.WriteLine("Build Output")
10+
.WriteRule("Results")
11+
.WriteTable(t => t
12+
.AddColumn("Test")
13+
.AddColumn("Status")
14+
.AddRow("Unit", "PASSED".Green()))
15+
.WriteRule()
16+
.WriteLine("Done");
17+
```
18+
19+
## Checklist
20+
21+
- [ ] Change `IConsole` interface: `Write`, `WriteLine`, `WriteErrorLine` return `IConsole`
22+
- [ ] Change `ITerminal` interface: override return type to `ITerminal` where needed
23+
- [ ] Update `TimeWarpTerminal` implementation
24+
- [ ] Update `TestTerminal` implementation
25+
- [ ] Update `TestConsole` implementation
26+
- [ ] Update `TimeWarpConsole` implementation
27+
- [ ] Change extension methods (`WriteTable`, `WritePanel`, `WriteRule`) to return `ITerminal`
28+
- [ ] Update `Terminal` static class methods
29+
- [ ] Update samples to show fluent chaining
30+
- [ ] Verify build
31+
32+
## Notes
33+
34+
- Extension methods are backward compatible — existing void-ignoring callers still compile
35+
- Interface changes require updating all implementations — breaking change for external implementors
36+
- This is a beta package so breaking changes are acceptable
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Update dev-cli to support self-install and .bin prebuilt binary
2+
3+
## Description
4+
5+
Follow the Nuru repo pattern for dev-cli self-install and prebuilt binary support. Currently the dev-cli can only be invoked via `dotnet run --project tools/dev-cli` which is slow and cumbersome.
6+
7+
## Checklist
8+
9+
- [ ] Fix pre-existing MissingMethodException with TimeWarp.Nuru dependency
10+
- [ ] Add `dev --self-install` command to publish AOT binary to `.bin/`
11+
- [ ] Add `.bin/` to `.gitignore`
12+
- [ ] Set up PATH so `dev test` works from repo root (via .envrc or similar)
13+
- [ ] Verify `dev test`, `dev build`, `dev ci` all work from `.bin/dev`
14+
15+
## Notes
16+
17+
- Reference: `/home/steventcramer/worktrees/github.com/TimeWarpEngineering/timewarp-nuru` dev-cli implementation
18+
- The MissingMethodException is a pre-existing issue: `NuruCoreAppBuilder..ctor(NuruCoreApplicationOptions)` not found — likely a version mismatch with TimeWarp.Nuru NuGet

samples/hyperlink-widget.cs

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,65 +8,71 @@
88
// Get a terminal instance
99
ITerminal terminal = TimeWarpTerminal.Default;
1010

11-
terminal.WriteLine();
12-
terminal.WriteLine("OSC 8 Hyperlink Demo".Cyan().Bold());
13-
terminal.WriteLine("Demonstrates clickable hyperlinks in supported terminals");
14-
terminal.WriteLine();
15-
16-
// Check terminal support
17-
terminal.WriteLine($"Terminal hyperlink support: {(terminal.SupportsHyperlinks ? "✓ Yes".Green() : "✗ No".Yellow())}");
18-
terminal.WriteLine();
11+
terminal
12+
.WriteLine()
13+
.WriteLine("OSC 8 Hyperlink Demo".Cyan().Bold())
14+
.WriteLine("Demonstrates clickable hyperlinks in supported terminals")
15+
.WriteLine()
16+
.WriteLine($"Terminal hyperlink support: {(terminal.SupportsHyperlinks ? "✓ Yes".Green() : "✗ No".Yellow())}")
17+
.WriteLine();
1918

2019
// 1. Simple hyperlink using string extension
21-
terminal.WriteLine("1. String extension - Link():");
22-
terminal.WriteLine($" Visit {"Ardalis.com".Link("https://ardalis.com")}");
23-
terminal.WriteLine();
20+
terminal
21+
.WriteLine("1. String extension - Link():")
22+
.WriteLine($" Visit {"Ardalis.com".Link("https://ardalis.com")}")
23+
.WriteLine();
2424

2525
// 2. Terminal extension methods
26-
terminal.WriteLine("2. Terminal extension - WriteLink():");
27-
terminal.Write(" Check out: ");
28-
terminal.WriteLink("https://github.com", "GitHub");
29-
terminal.WriteLine();
30-
terminal.WriteLine();
26+
terminal
27+
.WriteLine("2. Terminal extension - WriteLink():")
28+
.Write(" Check out: ")
29+
.WriteLink("https://github.com", "GitHub")
30+
.WriteLine()
31+
.WriteLine();
3132

3233
// 3. WriteLinkLine with just URL (URL as display text)
33-
terminal.WriteLine("3. URL as display text:");
34-
terminal.Write(" ");
35-
terminal.WriteLinkLine("https://docs.microsoft.com/dotnet");
36-
terminal.WriteLine();
34+
terminal
35+
.WriteLine("3. URL as display text:")
36+
.Write(" ")
37+
.WriteLinkLine("https://docs.microsoft.com/dotnet")
38+
.WriteLine();
3739

3840
// 4. Chaining with color extensions
39-
terminal.WriteLine("4. Hyperlinks with styling:");
40-
terminal.WriteLine($" {"Download here".Link("https://example.com/download").Blue().Underline()}");
41-
terminal.WriteLine($" {"Read the docs".Link("https://docs.example.com").Cyan().Bold()}");
42-
terminal.WriteLine($" {"Report a bug".Link("https://github.com/issues").Yellow()}");
43-
terminal.WriteLine();
41+
terminal
42+
.WriteLine("4. Hyperlinks with styling:")
43+
.WriteLine($" {"Download here".Link("https://example.com/download").Blue().Underline()}")
44+
.WriteLine($" {"Read the docs".Link("https://docs.example.com").Cyan().Bold()}")
45+
.WriteLine($" {"Report a bug".Link("https://github.com/issues").Yellow()}")
46+
.WriteLine();
4447

4548
// 5. Multiple links in one line
46-
terminal.WriteLine("5. Multiple links in one line:");
47-
terminal.WriteLine($" {"Home".Link("https://example.com")} | {"About".Link("https://example.com/about")} | {"Contact".Link("https://example.com/contact")}");
48-
terminal.WriteLine();
49+
terminal
50+
.WriteLine("5. Multiple links in one line:")
51+
.WriteLine($" {"Home".Link("https://example.com")} | {"About".Link("https://example.com/about")} | {"Contact".Link("https://example.com/contact")}")
52+
.WriteLine();
4953

5054
// 6. Links in formatted text
51-
terminal.WriteLine("6. Links in formatted output:");
52-
terminal.WriteLine($" For more information, see the {"documentation".Link("https://docs.example.com").Cyan()}");
53-
terminal.WriteLine($" or visit our {"community forum".Link("https://forum.example.com").Green()}.");
54-
terminal.WriteLine();
55+
terminal
56+
.WriteLine("6. Links in formatted output:")
57+
.WriteLine($" For more information, see the {"documentation".Link("https://docs.example.com").Cyan()}")
58+
.WriteLine($" or visit our {"community forum".Link("https://forum.example.com").Green()}.")
59+
.WriteLine();
5560

5661
// 7. Practical example - CLI help with links
57-
terminal.WriteLine("7. Practical example - Help text with links:");
58-
terminal.WriteLine();
59-
terminal.WritePanel(panel => panel
60-
.Header("TimeWarp.Nuru".Cyan().Bold())
62+
terminal
63+
.WriteLine("7. Practical example - Help text with links:")
64+
.WriteLine()
65+
.WritePanel(panel => panel
66+
.Header("TimeWarp.Terminal".Cyan().Bold())
6167
.Content(
62-
"A fluent CLI framework for .NET 10\n\n" +
63-
$"Documentation: {"https://timewarp.dev/nuru".Link("https://timewarp.dev/nuru").Cyan()}\n" +
64-
$"Source: {"GitHub".Link("https://github.com/TimeWarpEngineering/timewarp-nuru").Cyan()}\n" +
65-
$"Issues: {"Report bugs".Link("https://github.com/TimeWarpEngineering/timewarp-nuru/issues").Yellow()}")
68+
"Terminal abstractions and widgets for .NET 10\n\n" +
69+
$"Documentation: {"https://timewarp.dev/terminal".Link("https://timewarp.dev/terminal").Cyan()}\n" +
70+
$"Source: {"GitHub".Link("https://github.com/TimeWarpEngineering/timewarp-terminal").Cyan()}\n" +
71+
$"Issues: {"Report bugs".Link("https://github.com/TimeWarpEngineering/timewarp-terminal/issues").Yellow()}")
6672
.Border(BorderStyle.Rounded)
6773
.BorderColor(AnsiColors.Cyan)
68-
.Padding(2, 1));
69-
terminal.WriteLine();
74+
.Padding(2, 1))
75+
.WriteLine();
7076

7177
// 8. Conditional hyperlinks
7278
terminal.WriteLine("8. Graceful degradation:");
@@ -76,18 +82,20 @@
7682
}
7783
else
7884
{
79-
terminal.WriteLine(" Your terminal doesn't support OSC 8 hyperlinks.");
80-
terminal.WriteLine(" The text still displays, but won't be clickable.");
81-
terminal.WriteLine(" Supported terminals: Windows Terminal, iTerm2, VS Code, Hyper, Konsole, GNOME Terminal 3.26+");
85+
terminal
86+
.WriteLine(" Your terminal doesn't support OSC 8 hyperlinks.")
87+
.WriteLine(" The text still displays, but won't be clickable.")
88+
.WriteLine(" Supported terminals: Windows Terminal, iTerm2, VS Code, Hyper, Konsole, GNOME Terminal 3.26+");
8289
}
83-
terminal.WriteLine();
8490

85-
// 9. Technical details
86-
terminal.WriteLine("9. OSC 8 escape sequence format:");
87-
terminal.WriteLine(" \\e]8;;URL\\e\\\\DISPLAY_TEXT\\e]8;;\\e\\\\".Gray());
8891
terminal.WriteLine();
8992

90-
terminal.WriteLine("Demo complete! Try running this in different terminals to see hyperlink support.".Gray());
91-
terminal.WriteLine();
93+
// 9. Technical details
94+
terminal
95+
.WriteLine("9. OSC 8 escape sequence format:")
96+
.WriteLine(" \\e]8;;URL\\e\\\\DISPLAY_TEXT\\e]8;;\\e\\\\".Gray())
97+
.WriteLine()
98+
.WriteLine("Demo complete! Try running this in different terminals to see hyperlink support.".Gray())
99+
.WriteLine();
92100

93101
return 0;

0 commit comments

Comments
 (0)