---
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` (widgets). Companion: `TimeWarp.Terminal.Layout` (flexbox composition).

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` |
| Side-by-side panels / dashboard rows | Companion package `TimeWarp.Terminal.Layout` — `WriteLayout` |

## Installation

```bash
dotnet add package TimeWarp.Terminal
```

## Core Pattern: Dependency Injection

```csharp
// Production
services.AddSingleton<ITerminal>(TimeWarpTerminal.Default);

// Testing
services.AddSingleton<ITerminal>(new TestTerminal());
```

## Quick Examples

### Basic Output

```csharp
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.

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

### Layout (companion package)

`TimeWarp.Terminal.Layout` composes existing widgets into flexbox rows/columns. It is **not** a dependency of `TimeWarp.Terminal`. Types live in the `TimeWarp.Terminal` namespace (`WriteLayout`, `LayoutBuilder`, `Layout`).

```bash
dotnet add package TimeWarp.Terminal.Layout
```

```csharp
using TimeWarp.Terminal;
using TimeWarp.Flexbox;

terminal.WriteLayout(layout => layout
  .Direction(FlexDirection.Row)
  .Gap(2)
  .Item(i => i.Grow(1), panel => panel.Header("Build").Content(buildSummary))
  .Item(i => i.Grow(2), table => table.AddColumns("Test", "Result").AddRow("unit", "pass")));

terminal.WriteLayout(layout => layout
  .Direction(FlexDirection.Column)
  .Row(r => r.Item(statusPanel).Item(versionPanel))
  .Row(r => r.Item(i => i.Grow(1), logTable)));
```

Static facade: `TerminalLayout.WriteLayout(...)`. Sizing uses `WindowWidth`. Color overloads gate on `SupportsColor`. Items never collapse to width 0.

### Hyperlinks

```csharp
// 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 (same graceful degradation via SupportsHyperlinks)
Terminal.WriteLink("https://example.com", "Click here");
Terminal.WriteLinkLine("https://example.com", "Visit site");

// Low-level — parameter order matches WriteLink: (url, displayText)
string osc8 = AnsiHyperlinks.CreateLink("https://example.com", "text");

```

### Colors and Styles

```csharp
// 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/WriteTable
// (Write, WriteLine, and WriteErrorLine all have fg and fg+bg overloads)
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.
Inside `TestTerminalContext.Use`, that assignment is async-local (parallel-safe);
outside a context it is process-global.

### 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.

```csharp
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

```csharp
// 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

Use `TestTerminalContext.Use` — it is async-context isolated (safe for parallel tests,
never mutates the global) and restores automatically on dispose:

```csharp
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:
```csharp
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

```csharp
// 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?` | `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

```csharp
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

```csharp
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.

```csharp
// 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

```csharp
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.

```csharp
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. **Prefer `TestTerminalContext.Use` over swapping `Terminal.Instance`** - the scope is parallel-safe and restores automatically; direct swaps require manual restore and are serial-only
4. **`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
5. **Use `UnicodeWidth.GetTextWidth()` not `.Length`** for terminal column calculations — `.Length` counts UTF-16 code units, not display columns
