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));
// 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

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

  1. Don't use new Table(), new Panel(), new Rule() - Constructors are internal. Use builders or Action<XxxBuilder> extension methods.
  2. Don't mix Console and Terminal - Pick one, stick with it
  3. Always restore Terminal.Instance in tests - Use try/finally or using pattern
  4. Check SupportsColor/SupportsHyperlinks before using those features in production
  5. Use UnicodeWidth.GetTextWidth() not .Length for terminal column calculations — .Length counts UTF-16 code units, not display columns