TimeWarp.Terminal library - console abstractions (IConsole, ITerminal), widgets (panels, tables, rules), ANSI colors, hyperlinks, and Unicode width handling for testable C# console apps
Maintained in timewarp-terminal · Canonical file: SKILL.md
Install
npx skills add TimeWarpEngineering/timewarp-terminal --skill terminal
Or copy the SKILL.md into your agent's skills directory.
TimeWarp.Terminal
Console abstractions and widgets for testable C# applications.
Repository: https://github.com/TimeWarpEngineering/timewarp-terminal
Package: TimeWarp.Terminal
For detailed API documentation, fetch the README from the repository.
When to Use What
| Need | Use |
|---|---|
| Basic testable I/O | IConsole |
| Rich output (colors, widgets, hyperlinks) | ITerminal |
| Quick utility script, easy Console migration | Terminal static class |
| Unit testing | TestTerminal or TestConsole |
| Measure terminal display width | UnicodeWidth |
| Strip/measure ANSI strings | AnsiStringUtils |
Installation
dotnet add package TimeWarp.Terminal
Core Pattern: Dependency Injection
// Production
services.AddSingleton<ITerminal>(TimeWarpTerminal.Default);
// Testing
services.AddSingleton<ITerminal>(new TestTerminal());
Quick Examples
Basic Output
using TimeWarp.Terminal;
// Static API (Console replacement)
Terminal.WriteLine("Hello".Green());
Terminal.WriteLine("Warning".Yellow().Bold());
// Instance API (testable) — all Write methods return ITerminal for fluent chaining
ITerminal terminal = TimeWarpTerminal.Default;
terminal
.WriteLine("Hello".Cyan())
.WriteRule("Section")
.WriteLine("World".Green());
Widgets
All widgets use the builder pattern. Constructors for Table, Panel, and Rule are internal — always use the builder or Action<XxxBuilder> overloads.
// Panel — simple
terminal.WritePanel("Content here");
// Panel — with header parameter
terminal.WritePanel("Content here", "Title");
// Panel with builder
terminal.WritePanel(panel => panel
.Header("Configuration")
.Content("Setting: value")
.Border(BorderStyle.Rounded)
.Padding(2, 1));
// Table
terminal.WriteTable(table => table
.AddColumn("Name")
.AddColumn("Status", Alignment.Right)
.AddRow("API", "Online".Green())
.AddRow("DB", "Offline".Red())
.Border(BorderStyle.Rounded));
// Standalone builder (when you need the Table object)
Table table = new TableBuilder()
.AddColumn("Name")
.AddRow("foo")
.Border(BorderStyle.Rounded)
.Build();
terminal.WriteTable(table);
// Rule (section divider)
terminal.WriteRule("Section Title".Cyan());
// Rule with style
terminal.WriteRule("Configuration", style: LineStyle.Doubled);
// Rule with builder
terminal.WriteRule(rule => rule
.Title("Configuration")
.Style(LineStyle.Doubled)
.Color(AnsiColors.Cyan));
Hyperlinks
// String extension — creates OSC 8 hyperlink
string link = "Click here".Link("https://example.com");
// With styling
string styledLink = "Docs".Link("https://docs.example.com").Blue().Underline();
// Terminal extension methods (gracefully degrade if unsupported)
terminal.WriteLink("https://example.com", "Click here"); // no newline
terminal.WriteLinkLine("https://example.com", "Visit site"); // with newline
// Static API
Terminal.WriteLink("https://example.com", "Click here");
// Check support
if (terminal.SupportsHyperlinks)
terminal.WriteLinkLine("https://example.com", "Link");
else
terminal.WriteLine("https://example.com");
// Low-level
string osc8 = AnsiHyperlinks.CreateLink("text", "https://example.com");
Colors and Styles
// Colors: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Gray
// Bright variants: BrightRed, BrightGreen, etc.
// Styles: Bold(), Dim(), Italic(), Underline(), Strikethrough()
// Background: OnRed(), OnGreen(), OnYellow(), etc.
terminal.WriteLine("Success".Green().Bold());
terminal.WriteLine("Error".Red().OnWhite());
// ConsoleColor overloads on static API and WritePanel
Terminal.WriteLine("colored", ConsoleColor.Green);
Terminal.WriteLine("colored", ConsoleColor.Red, ConsoleColor.White);
Unicode Width
Calculates terminal display width accounting for emoji, CJK, fullwidth forms, and zero-width characters. Uses exact Emoji_Presentation=Yes code points from Unicode 16.0.
UnicodeWidth.GetTextWidth("Hello"); // 5 (ASCII)
UnicodeWidth.GetTextWidth("📍"); // 2 (emoji)
UnicodeWidth.GetTextWidth("漢字"); // 4 (CJK)
UnicodeWidth.GetTextWidth("📍 Location"); // 11 (mixed)
UnicodeWidth.GetTextWidth("🇺🇸"); // 2 (flag - multi-codepoint cluster)
UnicodeWidth.GetTextWidth("☁️"); // 2 (text char + VS16)
UnicodeWidth.GetRuneWidth(new Rune('A')); // 1
UnicodeWidth.GetRuneWidth(new Rune('漢')); // 2
UnicodeWidth.GetRuneWidth(new Rune(0x200D)); // 0 (ZWJ)
Testing
// Create test terminal with scripted input
using TestTerminal terminal = new("yes\n");
// Run code under test
MyCommand command = new(terminal);
command.Execute();
// Verify output
Assert.Contains("expected text", terminal.Output);
Assert.Contains("error message", terminal.ErrorOutput);
Static Terminal Testing
using TestTerminal testTerminal = new();
Terminal.Instance = testTerminal;
try
{
Terminal.WriteLine("test");
Assert.Contains("test", testTerminal.Output);
}
finally
{
Terminal.Instance = TimeWarpTerminal.Default;
}
Key Interfaces
IConsole: Write -> IConsole, WriteLine -> IConsole, WriteLineAsync -> Task, WriteErrorLine -> IConsole, WriteErrorLineAsync -> Task, ReadLine
ITerminal extends IConsole: Overrides Write -> ITerminal, WriteLine -> ITerminal, WriteErrorLine -> ITerminal. Adds ReadKey, SetCursorPosition, GetCursorPosition, WindowWidth, IsInteractive, SupportsColor, SupportsHyperlinks, Clear
All Write methods return the interface for fluent chaining:
terminal
.WriteLine("Build Output")
.WriteRule("Results")
.WriteTable(t => t
.AddColumn("Test")
.AddColumn("Status")
.AddRow("Unit", "PASSED".Green()))
.WriteRule()
.WriteLine("Done!");
Widget Builder API
Widget constructors are internal. Always use builders.
PanelBuilder: .Header(), .Content(string?), .Border(), .BorderColor(), .Padding(), .PaddingHorizontal(), .PaddingVertical(), .Width(), .WordWrap(), .Build()
TableBuilder: .AddColumn(name), .AddColumn(name, alignment), .AddColumn(TableColumn), .AddColumns(params string[]), .AddColumns(params TableColumn[]), .AddRow(params), .Border(), .BorderColor(), .HideHeaders(), .ShowRowSeparators(), .Expand(), .Build()
RuleBuilder: .Title(), .Style(), .Color(), .Width(), .Build()
WritePanel Overloads
// Simple content
terminal.WritePanel("Content");
terminal.WritePanel("Content", BorderStyle.Rounded);
// With header parameter
terminal.WritePanel("Content", "Header");
terminal.WritePanel("Content", "Header", BorderStyle.Doubled);
// With colors
terminal.WritePanel("Content", "Header", BorderStyle.Rounded, ConsoleColor.Green, ConsoleColor.Black);
// Builder pattern
terminal.WritePanel(p => p.Header("Title").Content("Body").Border(BorderStyle.Rounded));
// Pre-built panel
terminal.WritePanel(panel);
TableColumn Properties
| Property | Type | Default | Description |
|---|---|---|---|
Header |
string |
"" |
Column header text (supports ANSI colors) |
Alignment |
Alignment |
Left |
Horizontal alignment (Left, Right, Center) |
MinWidth |
int? |
4 |
Column won't shrink below this width |
MaxWidth |
int? |
null |
Content exceeding this is truncated with ellipsis |
TruncateMode |
TruncateMode |
End |
Where to place ellipsis when truncating |
HeaderColor |
string? |
null |
ANSI color code for header text |
Grow |
bool |
false |
When true, column expands to fill remaining terminal width after fixed columns are allocated. Multiple grow columns share remaining space evenly. |
TruncateMode (Ellipsis Placement)
| Mode | Result | Use case |
|---|---|---|
TruncateMode.End |
"long text..." |
Default — shows beginning |
TruncateMode.Start |
"...long text" |
File paths — shows the meaningful end |
TruncateMode.Middle |
"long...text" |
Shows both start and end |
Example: All three modes side-by-side
string longText = "This-is-a-very-long-text-that-will-be-truncated-differently";
terminal.WriteTable(t => t
.AddColumn(new TableColumn("Mode") { MaxWidth = 8 })
.AddColumn(new TableColumn("End (default)") { MaxWidth = 25, TruncateMode = TruncateMode.End })
.AddColumn(new TableColumn("Start") { MaxWidth = 25, TruncateMode = TruncateMode.Start })
.AddColumn(new TableColumn("Middle") { MaxWidth = 25, TruncateMode = TruncateMode.Middle })
.AddRow("Result", longText, longText, longText)
.Border(BorderStyle.Rounded));
Example: File paths with TruncateMode.Start
terminal.WriteTable(t => t
.AddColumn("Repository")
.AddColumn(new TableColumn("Worktree Path") { TruncateMode = TruncateMode.Start })
.AddColumn("Branch")
.AddRow("timewarp-nuru",
"/home/user/worktrees/github.com/TimeWarpEngineering/timewarp-nuru/feature-branch-name",
"feature-xyz")
.Border(BorderStyle.Rounded));
// Path column shows: "...timewarp-nuru/feature-branch-name"
Example: Grow column for flexbox-style layouts
Grow columns expand to fill remaining terminal width after fixed columns are allocated. Multiple grow columns share remaining space evenly.
// Single grow column fills remaining space
terminal.WriteTable(t => t
.AddColumn("ID")
.AddColumn(new TableColumn("Description") { Grow = true })
.AddColumn("Status")
.AddRow("1", "This description expands to fill available space", "Active")
.AddRow("2", "Another long description that grows", "Pending")
.Border(BorderStyle.Rounded));
// Multiple grow columns share remaining space evenly
terminal.WriteTable(t => t
.AddColumn("ID")
.AddColumn(new TableColumn("Left") { Grow = true })
.AddColumn(new TableColumn("Right") { Grow = true })
.AddRow("1", "Left side content", "Right side content")
.Border(BorderStyle.Rounded));
Example: Column with MinWidth constraint
terminal.WriteTable(t => t
.AddColumn("ID")
.AddColumn(new TableColumn("Description") { MinWidth = 20 })
.AddRow("1", "This is a long description that would normally be truncated heavily"));
Table Expand/Grow Behavior
- Shrink (always on): Proportionally reduces column widths to fit terminal. Wider columns shrink more aggressively. Respects
MinWidthper column. - Expand: Distributes extra terminal width evenly across all columns.
- Grow (column-level): When
Grow = trueon a column, it expands to fill remaining terminal width after fixed columns are allocated. Multiple grow columns share remaining space evenly. Takes precedence over Expand.
BorderStyle: Rounded, Square, Doubled, Heavy, None
Alignment: Left, Right, Center
AnsiStringUtils
Static utility class for ANSI-aware string operations. Handles CSI sequences and OSC 8 hyperlinks.
AnsiStringUtils.StripAnsiCodes("\x1b[32mGreen\x1b[0m"); // "Green"
AnsiStringUtils.GetVisibleLength("\x1b[32mGreen\x1b[0m"); // 5
AnsiStringUtils.GetVisibleLength("📍 Location"); // 11 (uses UnicodeWidth)
AnsiStringUtils.PadRightVisible("\x1b[32mHi\x1b[0m", 10); // pads to 10 visible chars
AnsiStringUtils.PadLeftVisible("\x1b[32mHi\x1b[0m", 10); // left-pads to 10
AnsiStringUtils.CenterVisible("\x1b[32mHi\x1b[0m", 10); // centers to 10
AnsiStringUtils.WrapText("long text...", 40); // word-wrap, grapheme-aware
Common Pitfalls
- Don't use
new Table(),new Panel(),new Rule()- Constructors are internal. Use builders orAction<XxxBuilder>extension methods. - Don't mix Console and Terminal - Pick one, stick with it
- Always restore Terminal.Instance in tests - Use try/finally or using pattern
- Check SupportsColor/SupportsHyperlinks before using those features in production
- Use
UnicodeWidth.GetTextWidth()not.Lengthfor terminal column calculations —.Lengthcounts UTF-16 code units, not display columns