| name | terminal |
| description | TimeWarp.Terminal library - console abstractions (IConsole, ITerminal), widgets (panels, tables, rules), ANSI colors, hyperlinks, and Unicode width handling for testable C# console apps |
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
services.AddSingleton<ITerminal>(TimeWarpTerminal.Default);
services.AddSingleton<ITerminal>(new TestTerminal());
Quick Examples
Basic Output
using TimeWarp.Terminal;
Terminal.WriteLine("Hello".Green());
Terminal.WriteLine("Warning".Yellow().Bold());
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.
terminal.WritePanel("Content here");
terminal.WritePanel("Content here", "Title");
terminal.WritePanel(panel => panel
.Header("Configuration")
.Content("Setting: value")
.Border(BorderStyle.Rounded)
.Padding(2, 1));
terminal.WriteTable(table => table
.AddColumn("Name")
.AddColumn("Status", Alignment.Right)
.AddRow("API", "Online".Green())
.AddRow("DB", "Offline".Red())
.Border(BorderStyle.Rounded));
Table table = new TableBuilder()
.AddColumn("Name")
.AddRow("foo")
.Border(BorderStyle.Rounded)
.Build();
terminal.WriteTable(table);
terminal.WriteRule("Section Title".Cyan());
terminal.WriteRule("Configuration", style: LineStyle.Doubled);
terminal.WriteRule(rule => rule
.Title("Configuration")
.Style(LineStyle.Doubled)
.Color(AnsiColors.Cyan));
Hyperlinks
string link = "Click here".Link("https://example.com");
string styledLink = "Docs".Link("https://docs.example.com").Blue().Underline();
terminal.WriteLink("https://example.com", "Click here");
terminal.WriteLinkLine("https://example.com", "Visit site");
Terminal.WriteLink("https://example.com", "Click here");
Terminal.WriteLinkLine("https://example.com", "Visit site");
string osc8 = AnsiHyperlinks.CreateLink("https://example.com", "text");
Colors and Styles
terminal.WriteLine("Success".Green().Bold());
terminal.WriteLine("Error".Red().OnWhite());
Terminal.WriteLine("colored", ConsoleColor.Green);
Terminal.WriteLine("colored", ConsoleColor.Red, ConsoleColor.White);
ConsoleColor overloads and widget color parameters automatically degrade to plain text
when SupportsColor is false (redirected output, non-empty NO_COLOR, or TERM=dumb).
Embedded ANSI from string extensions like .Green() is the caller's responsibility.
Format Culture
Format overloads (Terminal.Write("{0:N2}", value) etc.) use the current culture,
matching System.Console. Set Terminal.FormatProvider = CultureInfo.InvariantCulture
for deterministic output; null (default) resolves CurrentCulture per call.
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");
UnicodeWidth.GetTextWidth("📍");
UnicodeWidth.GetTextWidth("漢字");
UnicodeWidth.GetTextWidth("📍 Location");
UnicodeWidth.GetTextWidth("🇺🇸");
UnicodeWidth.GetTextWidth("☁️");
UnicodeWidth.GetRuneWidth(new Rune('A'));
UnicodeWidth.GetRuneWidth(new Rune('漢'));
UnicodeWidth.GetRuneWidth(new Rune(0x200D));
Testing
using TestTerminal terminal = new("yes\n");
MyCommand command = new(terminal);
command.Execute();
Assert.Contains("expected text", terminal.Output);
Assert.Contains("error message", terminal.ErrorOutput);
Static Terminal Testing
Use TestTerminalContext.Use — it is async-context isolated (safe for parallel tests,
never mutates the global) and restores automatically on dispose:
using TestTerminal terminal = new();
using IDisposable scope = TestTerminalContext.Use(terminal);
Terminal.WriteLine("test");
Assert.Contains("test", terminal.Output);
For strictly serial tests, direct Terminal.Instance = testTerminal assignment also
works, but you must restore the original instance yourself.
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() / ReadKey(bool) (key-by-key input lives here, not on IConsole), SetCursorPosition, GetCursorPosition, WindowWidth/WindowHeight/BufferWidth/BufferHeight (get-only; TestTerminal exposes setters for test configuration), CancelKeyPress, 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
terminal.WritePanel("Content");
terminal.WritePanel("Content", BorderStyle.Rounded);
terminal.WritePanel("Content", "Header");
terminal.WritePanel("Content", "Header", BorderStyle.Doubled);
terminal.WritePanel("Content", "Header", BorderStyle.Rounded, ConsoleColor.Green, ConsoleColor.Black);
terminal.WritePanel(p => p.Header("Title").Content("Body").Border(BorderStyle.Rounded));
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? | null | Column won't shrink below this width; the layout floor is max(4, MinWidth) |
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));
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.
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));
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
MinWidth per column.
- Expand: Distributes extra terminal width evenly across all columns.
- Grow (column-level): When
Grow = true on 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");
AnsiStringUtils.GetVisibleLength("\x1b[32mGreen\x1b[0m");
AnsiStringUtils.GetVisibleLength("📍 Location");
AnsiStringUtils.PadRightVisible("\x1b[32mHi\x1b[0m", 10);
AnsiStringUtils.PadLeftVisible("\x1b[32mHi\x1b[0m", 10);
AnsiStringUtils.CenterVisible("\x1b[32mHi\x1b[0m", 10);
AnsiStringUtils.WrapText("long text...", 40);
Common Pitfalls
- Don't use
new Table(), new Panel(), new Rule() - Constructors are internal. Use builders or Action<XxxBuilder> extension methods.
- Don't mix Console and Terminal - Pick one, stick with it
- Prefer
TestTerminalContext.Use over swapping Terminal.Instance - the scope is parallel-safe and restores automatically; direct swaps require manual restore and are serial-only
ConsoleColor overloads and WriteLink self-gate on SupportsColor/SupportsHyperlinks — but embedded ANSI from string extensions (.Green(), BorderColor) does not; check SupportsColor yourself before emitting those
- Use
UnicodeWidth.GetTextWidth() not .Length for terminal column calculations — .Length counts UTF-16 code units, not display columns