Skip to content

Commit 31ac8b0

Browse files
Merge pull request #10 from TimeWarpEngineering/Cramer/2025-12-22/dev
docs: add terminal SKILL.md
2 parents 0a205cb + 795a790 commit 31ac8b0

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

skills/terminal/SKILL.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
---
2+
name: terminal
3+
description: TimeWarp.Terminal library - console abstractions (IConsole, ITerminal), widgets (panels, tables, rules), ANSI colors, hyperlinks, and Unicode width handling for testable C# console apps
4+
---
5+
6+
# TimeWarp.Terminal
7+
8+
Console abstractions and widgets for testable C# applications.
9+
10+
**Repository:** https://github.com/TimeWarpEngineering/timewarp-terminal
11+
**Package:** `TimeWarp.Terminal`
12+
13+
For detailed API documentation, fetch the README from the repository.
14+
15+
## When to Use What
16+
17+
| Need | Use |
18+
|------|-----|
19+
| Basic testable I/O | `IConsole` |
20+
| Rich output (colors, widgets, hyperlinks) | `ITerminal` |
21+
| Quick utility script, easy Console migration | `Terminal` static class |
22+
| Unit testing | `TestTerminal` or `TestConsole` |
23+
| Measure terminal display width | `UnicodeWidth` |
24+
| Strip/measure ANSI strings | `AnsiStringUtils` |
25+
26+
## Installation
27+
28+
```bash
29+
dotnet add package TimeWarp.Terminal
30+
```
31+
32+
## Core Pattern: Dependency Injection
33+
34+
```csharp
35+
// Production
36+
services.AddSingleton<ITerminal>(TimeWarpTerminal.Default);
37+
38+
// Testing
39+
services.AddSingleton<ITerminal>(new TestTerminal());
40+
```
41+
42+
## Quick Examples
43+
44+
### Basic Output
45+
46+
```csharp
47+
using TimeWarp.Terminal;
48+
49+
// Static API (Console replacement)
50+
Terminal.WriteLine("Hello".Green());
51+
Terminal.WriteLine("Warning".Yellow().Bold());
52+
53+
// Instance API (testable) — all Write methods return ITerminal for fluent chaining
54+
ITerminal terminal = TimeWarpTerminal.Default;
55+
terminal
56+
.WriteLine("Hello".Cyan())
57+
.WriteRule("Section")
58+
.WriteLine("World".Green());
59+
```
60+
61+
### Widgets
62+
63+
All widgets use the builder pattern. Constructors for `Table`, `Panel`, and `Rule` are internal — always use the builder or `Action<XxxBuilder>` overloads.
64+
65+
```csharp
66+
// Panel — simple
67+
terminal.WritePanel("Content here");
68+
69+
// Panel — with header parameter
70+
terminal.WritePanel("Content here", "Title");
71+
72+
// Panel with builder
73+
terminal.WritePanel(panel => panel
74+
.Header("Configuration")
75+
.Content("Setting: value")
76+
.Border(BorderStyle.Rounded)
77+
.Padding(2, 1));
78+
79+
// Table
80+
terminal.WriteTable(table => table
81+
.AddColumn("Name")
82+
.AddColumn("Status", Alignment.Right)
83+
.AddRow("API", "Online".Green())
84+
.AddRow("DB", "Offline".Red())
85+
.Border(BorderStyle.Rounded));
86+
87+
// Standalone builder (when you need the Table object)
88+
Table table = new TableBuilder()
89+
.AddColumn("Name")
90+
.AddRow("foo")
91+
.Border(BorderStyle.Rounded)
92+
.Build();
93+
terminal.WriteTable(table);
94+
95+
// Rule (section divider)
96+
terminal.WriteRule("Section Title".Cyan());
97+
98+
// Rule with style
99+
terminal.WriteRule("Configuration", style: LineStyle.Doubled);
100+
101+
// Rule with builder
102+
terminal.WriteRule(rule => rule
103+
.Title("Configuration")
104+
.Style(LineStyle.Doubled)
105+
.Color(AnsiColors.Cyan));
106+
```
107+
108+
### Hyperlinks
109+
110+
```csharp
111+
// String extension — creates OSC 8 hyperlink
112+
string link = "Click here".Link("https://example.com");
113+
114+
// With styling
115+
string styledLink = "Docs".Link("https://docs.example.com").Blue().Underline();
116+
117+
// Terminal extension methods (gracefully degrade if unsupported)
118+
terminal.WriteLink("https://example.com", "Click here"); // no newline
119+
terminal.WriteLinkLine("https://example.com", "Visit site"); // with newline
120+
121+
// Static API
122+
Terminal.WriteLink("https://example.com", "Click here");
123+
124+
// Check support
125+
if (terminal.SupportsHyperlinks)
126+
terminal.WriteLinkLine("https://example.com", "Link");
127+
else
128+
terminal.WriteLine("https://example.com");
129+
130+
// Low-level
131+
string osc8 = AnsiHyperlinks.CreateLink("text", "https://example.com");
132+
```
133+
134+
### Colors and Styles
135+
136+
```csharp
137+
// Colors: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Gray
138+
// Bright variants: BrightRed, BrightGreen, etc.
139+
// Styles: Bold(), Dim(), Italic(), Underline(), Strikethrough()
140+
// Background: OnRed(), OnGreen(), OnYellow(), etc.
141+
142+
terminal.WriteLine("Success".Green().Bold());
143+
terminal.WriteLine("Error".Red().OnWhite());
144+
145+
// ConsoleColor overloads on static API and WritePanel
146+
Terminal.WriteLine("colored", ConsoleColor.Green);
147+
Terminal.WriteLine("colored", ConsoleColor.Red, ConsoleColor.White);
148+
```
149+
150+
### Unicode Width
151+
152+
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.
153+
154+
```csharp
155+
UnicodeWidth.GetTextWidth("Hello"); // 5 (ASCII)
156+
UnicodeWidth.GetTextWidth("📍"); // 2 (emoji)
157+
UnicodeWidth.GetTextWidth("漢字"); // 4 (CJK)
158+
UnicodeWidth.GetTextWidth("📍 Location"); // 11 (mixed)
159+
UnicodeWidth.GetTextWidth("🇺🇸"); // 2 (flag - multi-codepoint cluster)
160+
UnicodeWidth.GetTextWidth("☁️"); // 2 (text char + VS16)
161+
162+
UnicodeWidth.GetRuneWidth(new Rune('A')); // 1
163+
UnicodeWidth.GetRuneWidth(new Rune('漢')); // 2
164+
UnicodeWidth.GetRuneWidth(new Rune(0x200D)); // 0 (ZWJ)
165+
```
166+
167+
### Testing
168+
169+
```csharp
170+
// Create test terminal with scripted input
171+
using TestTerminal terminal = new("yes\n");
172+
173+
// Run code under test
174+
MyCommand command = new(terminal);
175+
command.Execute();
176+
177+
// Verify output
178+
Assert.Contains("expected text", terminal.Output);
179+
Assert.Contains("error message", terminal.ErrorOutput);
180+
```
181+
182+
### Static Terminal Testing
183+
184+
```csharp
185+
using TestTerminal testTerminal = new();
186+
Terminal.Instance = testTerminal;
187+
try
188+
{
189+
Terminal.WriteLine("test");
190+
Assert.Contains("test", testTerminal.Output);
191+
}
192+
finally
193+
{
194+
Terminal.Instance = TimeWarpTerminal.Default;
195+
}
196+
```
197+
198+
## Key Interfaces
199+
200+
**IConsole:** `Write` -> `IConsole`, `WriteLine` -> `IConsole`, `WriteLineAsync` -> `Task`, `WriteErrorLine` -> `IConsole`, `WriteErrorLineAsync` -> `Task`, `ReadLine`
201+
202+
**ITerminal extends IConsole:** Overrides `Write` -> `ITerminal`, `WriteLine` -> `ITerminal`, `WriteErrorLine` -> `ITerminal`. Adds `ReadKey`, `SetCursorPosition`, `GetCursorPosition`, `WindowWidth`, `IsInteractive`, `SupportsColor`, `SupportsHyperlinks`, `Clear`
203+
204+
All Write methods return the interface for fluent chaining:
205+
```csharp
206+
terminal
207+
.WriteLine("Build Output")
208+
.WriteRule("Results")
209+
.WriteTable(t => t
210+
.AddColumn("Test")
211+
.AddColumn("Status")
212+
.AddRow("Unit", "PASSED".Green()))
213+
.WriteRule()
214+
.WriteLine("Done!");
215+
```
216+
217+
## Widget Builder API
218+
219+
Widget constructors are internal. Always use builders.
220+
221+
**PanelBuilder:** `.Header()`, `.Content(string?)`, `.Border()`, `.BorderColor()`, `.Padding()`, `.PaddingHorizontal()`, `.PaddingVertical()`, `.Width()`, `.WordWrap()`, `.Build()`
222+
223+
**TableBuilder:** `.AddColumn(name)`, `.AddColumn(name, alignment)`, `.AddColumn(TableColumn)`, `.AddColumns(params)`, `.AddRow(params)`, `.Border()`, `.BorderColor()`, `.HideHeaders()`, `.ShowRowSeparators()`, `.Expand()`, `.Shrink()`, `.Build()`
224+
225+
**RuleBuilder:** `.Title()`, `.Style()`, `.Color()`, `.Width()`, `.Build()`
226+
227+
### WritePanel Overloads
228+
229+
```csharp
230+
// Simple content
231+
terminal.WritePanel("Content");
232+
terminal.WritePanel("Content", BorderStyle.Rounded);
233+
234+
// With header parameter
235+
terminal.WritePanel("Content", "Header");
236+
terminal.WritePanel("Content", "Header", BorderStyle.Doubled);
237+
238+
// With colors
239+
terminal.WritePanel("Content", "Header", BorderStyle.Rounded, ConsoleColor.Green, ConsoleColor.Black);
240+
241+
// Builder pattern
242+
terminal.WritePanel(p => p.Header("Title").Content("Body").Border(BorderStyle.Rounded));
243+
244+
// Pre-built panel
245+
terminal.WritePanel(panel);
246+
```
247+
248+
### TableColumn Properties
249+
250+
| Property | Type | Default | Description |
251+
|----------|------|---------|-------------|
252+
| `Header` | `string` | `""` | Column header text (supports ANSI colors) |
253+
| `Alignment` | `Alignment` | `Left` | Horizontal alignment (`Left`, `Right`, `Center`) |
254+
| `MinWidth` | `int?` | `4` | Column won't shrink below this width |
255+
| `MaxWidth` | `int?` | `null` | Content exceeding this is truncated with ellipsis |
256+
| `TruncateMode` | `TruncateMode` | `End` | Where to place ellipsis when truncating |
257+
| `HeaderColor` | `string?` | `null` | ANSI color code for header text |
258+
259+
### TruncateMode (Ellipsis Placement)
260+
261+
| Mode | Result | Use case |
262+
|------|--------|----------|
263+
| `TruncateMode.End` | `"long text..."` | Default — shows beginning |
264+
| `TruncateMode.Start` | `"...long text"` | File paths — shows the meaningful end |
265+
| `TruncateMode.Middle` | `"long...text"` | Shows both start and end |
266+
267+
#### Example: All three modes side-by-side
268+
269+
```csharp
270+
string longText = "This-is-a-very-long-text-that-will-be-truncated-differently";
271+
272+
terminal.WriteTable(t => t
273+
.AddColumn(new TableColumn("Mode") { MaxWidth = 8 })
274+
.AddColumn(new TableColumn("End (default)") { MaxWidth = 25, TruncateMode = TruncateMode.End })
275+
.AddColumn(new TableColumn("Start") { MaxWidth = 25, TruncateMode = TruncateMode.Start })
276+
.AddColumn(new TableColumn("Middle") { MaxWidth = 25, TruncateMode = TruncateMode.Middle })
277+
.AddRow("Result", longText, longText, longText)
278+
.Border(BorderStyle.Rounded));
279+
```
280+
281+
#### Example: File paths with TruncateMode.Start
282+
283+
```csharp
284+
terminal.WriteTable(t => t
285+
.AddColumn("Repository")
286+
.AddColumn(new TableColumn("Worktree Path") { TruncateMode = TruncateMode.Start })
287+
.AddColumn("Branch")
288+
.AddRow("timewarp-nuru",
289+
"/home/user/worktrees/github.com/TimeWarpEngineering/timewarp-nuru/feature-branch-name",
290+
"feature-xyz")
291+
.Border(BorderStyle.Rounded));
292+
// Path column shows: "...timewarp-nuru/feature-branch-name"
293+
```
294+
295+
#### Example: Column with MinWidth constraint
296+
297+
```csharp
298+
terminal.WriteTable(t => t
299+
.AddColumn("ID")
300+
.AddColumn(new TableColumn("Description") { MinWidth = 20 })
301+
.AddRow("1", "This is a long description that would normally be truncated heavily"));
302+
```
303+
304+
### Table Shrink/Expand Behavior
305+
306+
- **Shrink** (default: `true`): Proportionally reduces column widths to fit terminal. Wider columns shrink more aggressively. Respects `MinWidth` per column.
307+
- **Expand**: Distributes extra terminal width evenly across columns.
308+
- Disable shrinking with builder `.Shrink(false)` to allow horizontal overflow.
309+
310+
**BorderStyle:** `Rounded`, `Square`, `Doubled`, `Heavy`, `None`
311+
312+
**Alignment:** `Left`, `Right`, `Center`
313+
314+
## AnsiStringUtils
315+
316+
Static utility class for ANSI-aware string operations. Handles CSI sequences and OSC 8 hyperlinks.
317+
318+
```csharp
319+
AnsiStringUtils.StripAnsiCodes("\x1b[32mGreen\x1b[0m"); // "Green"
320+
AnsiStringUtils.GetVisibleLength("\x1b[32mGreen\x1b[0m"); // 5
321+
AnsiStringUtils.GetVisibleLength("📍 Location"); // 11 (uses UnicodeWidth)
322+
AnsiStringUtils.PadRightVisible("\x1b[32mHi\x1b[0m", 10); // pads to 10 visible chars
323+
AnsiStringUtils.PadLeftVisible("\x1b[32mHi\x1b[0m", 10); // left-pads to 10
324+
AnsiStringUtils.CenterVisible("\x1b[32mHi\x1b[0m", 10); // centers to 10
325+
AnsiStringUtils.WrapText("long text...", 40); // word-wrap, grapheme-aware
326+
```
327+
328+
## Common Pitfalls
329+
330+
1. **Don't use `new Table()`, `new Panel()`, `new Rule()`** - Constructors are internal. Use builders or `Action<XxxBuilder>` extension methods.
331+
2. **Don't mix Console and Terminal** - Pick one, stick with it
332+
3. **Always restore Terminal.Instance in tests** - Use try/finally or using pattern
333+
4. **Check SupportsColor/SupportsHyperlinks** before using those features in production
334+
5. **Use `UnicodeWidth.GetTextWidth()` not `.Length`** for terminal column calculations — `.Length` counts UTF-16 code units, not display columns

0 commit comments

Comments
 (0)