Sync Upstream
nyaruka/phonenumbers
Sync this Go port with a new upstream google/libphonenumber release — regenerate the embedded metadata and reconcile the ported Java logic.
Step-by-step guide for creating new widgets in the Hex1b TUI library.
$ npx skills add mitchdenny/hex1b --skill widget-creator -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install mitchdenny/hex1b widget-creator --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.github/skills/widget-creator .claude/skills/widget-creator && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .claude/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creatorType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add mitchdenny/hex1b --skill widget-creator -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install mitchdenny/hex1b widget-creator --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.github/skills/widget-creator .agents/skills/widget-creator && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .agents/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mitchdenny/hex1b --skill widget-creator -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install mitchdenny/hex1b widget-creator --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.github/skills/widget-creator .cursor/skills/widget-creator && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .cursor/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/mitchdenny/hex1b.git --path .github/skills/widget-creator--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add mitchdenny/hex1b --skill widget-creator -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install mitchdenny/hex1b widget-creator --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.github/skills/widget-creator .gemini/skills/widget-creator && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .gemini/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install mitchdenny/hex1b widget-creatorInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add mitchdenny/hex1b --skill widget-creator -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .github/skills && cp -r skills-src/.github/skills/widget-creator .github/skills/widget-creator && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .github/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mitchdenny/hex1b --skill widget-creator -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install mitchdenny/hex1b widget-creator --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.github/skills/widget-creator .opencode/skills/widget-creator && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "widget-creator" agent skill from https://github.com/mitchdenny/hex1b/tree/main/.github/skills/widget-creator into .opencode/skills/widget-creator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "widget-creator", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
widget-creatorStep-by-step guide for creating new widgets in the Hex1b TUI library.
Widget Creator is an agent skill from mitchdenny/hex1b. Step-by-step guide for creating new widgets in the Hex1b TUI library. Use when implementing new widgets from scratch, including widget records, nodes, extension methods, theming, reconciliation, and tests.
Its SKILL.md is about 7.6k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Business, Finance & HR, covering Accounting and bookkeeping and Theming and dark mode. It works with React. The repository describes itself as: The .NET Terminal Application Stack. The licence is MIT.
7 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 98d8766. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
dotnetFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Widget Creator loads about 7.6k tokens when it runs. Until then it costs about 55 tokens; SKILL.md has 1,207 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from mitchdenny/hex1b at commit 98d8766, republished under its MIT licence (© mitchdenny). 1,207 words, ~7,570 tokens.
.claude/skills/widget-creator/SKILL.md (or your agent's skills folder).This skill provides a comprehensive step-by-step guide for AI coding agents to create new widgets in the Hex1b TUI library. Widgets are the building blocks of Hex1b applications, following a declarative pattern inspired by React.
Creating a widget in Hex1b involves several coordinated files:
| File | Purpose |
|---|---|
src/Hex1b/Widgets/{Name}Widget.cs | Immutable widget record (describes what to render) |
src/Hex1b/Nodes/{Name}Node.cs | Mutable node class (manages state, renders) |
src/Hex1b/{Name}Extensions.cs | Fluent API extension methods |
src/Hex1b/Theming/{Name}Theme.cs | Theme elements (colors, characters) |
tests/Hex1b.Tests/{Name}NodeTests.cs | Unit tests |
Widgets are immutable record types that describe the desired UI. They capture configuration and event handlers but contain no rendering logic.
Location: src/Hex1b/Widgets/{Name}Widget.cs
Key principles:
internal properties with init for optional configurationFunc<TEventArgs, Task>? patternthis with { } patternReconcile() to create/update the corresponding nodeGetExpectedNodeType() to return the node typeTemplate:
using Hex1b.Events;
using Hex1b.Nodes;
namespace Hex1b.Widgets;
/// <summary>
/// Brief description of what the widget does.
/// </summary>
/// <param name="PrimaryProperty">Description of the main property.</param>
public sealed record MyWidget(string PrimaryProperty) : Hex1bWidget
{
/// <summary>
/// ActionId for the activate action. Use "WidgetName.ActionName" naming
/// convention (PascalCase, omit "Widget" suffix from the widget name).
/// Define one static readonly ActionId per rebindable action.
/// </summary>
public static readonly ActionId Activate = new($"{nameof(MyWidget)}.{nameof(Activate)}");
/// <summary>
/// Optional configuration property.
/// </summary>
internal bool SomeOption { get; init; }
/// <summary>
/// Event handler for some action.
/// </summary>
internal Func<MyEventArgs, Task>? ActionHandler { get; init; }
/// <summary>
/// Sets a synchronous action handler.
/// </summary>
public MyWidget OnAction(Action<MyEventArgs> handler)
=> this with { ActionHandler = args => { handler(args); return Task.CompletedTask; } };
/// <summary>
/// Sets an asynchronous action handler.
/// </summary>
public MyWidget OnAction(Func<MyEventArgs, Task> handler)
=> this with { ActionHandler = handler };
internal override Hex1bNode Reconcile(Hex1bNode? existingNode, ReconcileContext context)
{
var node = existingNode as MyNode ?? new MyNode();
// Mark dirty if properties changed
if (node.PrimaryProperty != PrimaryProperty || node.SomeOption != SomeOption)
{
node.MarkDirty();
}
node.PrimaryProperty = PrimaryProperty;
node.SomeOption = SomeOption;
node.SourceWidget = this;
// Convert typed event handler to internal handler if needed
if (ActionHandler != null)
{
node.ActionCallback = async ctx =>
{
var args = new MyEventArgs(this, node, ctx);
await ActionHandler(args);
};
}
else
{
node.ActionCallback = null;
}
return node;
}
internal override Type GetExpectedNodeType() => typeof(MyNode);
}If your widget has event handlers, create a typed event args class.
Location: src/Hex1b/Events/{Name}EventArgs.cs
Template:
using Hex1b.Input;
using Hex1b.Widgets;
namespace Hex1b.Events;
/// <summary>
/// Event arguments for MyWidget actions.
/// </summary>
public sealed class MyEventArgs
{
/// <summary>
/// The widget that raised the event.
/// </summary>
public MyWidget Widget { get; }
/// <summary>
/// The node that raised the event.
/// </summary>
public MyNode Node { get; }
/// <summary>
/// The input binding context.
/// </summary>
public InputBindingActionContext Context { get; }
internal MyEventArgs(MyWidget widget, MyNode node, InputBindingActionContext context)
{
Widget = widget;
Node = node;
Context = context;
}
}Nodes are mutable classes that hold render state and perform actual rendering. They receive updates during reconciliation and implement layout and rendering.
Location: src/Hex1b/Nodes/{Name}Node.cs
Key principles:
MarkDirty() when internal state changesMeasure() to calculate sizeRender() to draw to terminalIsFocusable if the widget can receive focusConfigureDefaultBindings() for keyboard/mouse handlingLift state up when mutable state would benefit composites. If your widget owns non-trivial mutable state that a parent might want to read, write, or coordinate with — buffer text, selection index, navigation history, checked value — promote that state to a public class and have your widget implement
IStatefulWidget<TSelf, TState>so callers can pass an external instance via.State(...). SeeTextBoxWidget/TextBoxState,EditorWidget/EditorState,NavigatorWidget/NavigatorState,CheckboxWidget/CheckboxStatefor the canonical pattern. The composition guide has a worked example. Pure visual state (focus, hover, animation phase) stays internal to the node — only mutable user-facing data needs lifting.
Template:
using Hex1b.Input;
using Hex1b.Layout;
using Hex1b.Theming;
using Hex1b.Widgets;
namespace Hex1b;
/// <summary>
/// Render node for MyWidget.
/// </summary>
public sealed class MyNode : Hex1bNode
{
public string PrimaryProperty { get; set; } = "";
public bool SomeOption { get; set; }
/// <summary>
/// The source widget for typed event args.
/// </summary>
public MyWidget? SourceWidget { get; set; }
/// <summary>
/// Callback for the action event.
/// </summary>
public Func<InputBindingActionContext, Task>? ActionCallback { get; set; }
// Focus tracking (if widget is focusable)
private bool _isFocused;
public override bool IsFocused
{
get => _isFocused;
set
{
if (_isFocused != value)
{
_isFocused = value;
MarkDirty();
}
}
}
public override bool IsFocusable => true; // Set to false for non-interactive widgets
public override void ConfigureDefaultBindings(InputBindingsBuilder bindings)
{
if (ActionCallback != null)
{
// Use .Triggers() with an ActionId to make bindings rebindable by users.
// Naming convention: "WidgetName.ActionName" (PascalCase, omit "Widget" suffix).
// Define the ActionId as a static readonly field on the widget record:
// public static readonly ActionId Activate = new($"{nameof(MyWidget)}.{nameof(Activate)}");
bindings.Key(Hex1bKey.Enter).Triggers(MyWidget.Activate, ActionCallback, "Activate");
}
}
public override Size Measure(Constraints constraints)
{
// Calculate the desired size
var width = PrimaryProperty.Length;
var height = 1;
return constraints.Constrain(new Size(width, height));
}
public override void Render(Hex1bRenderContext context)
{
var theme = context.Theme;
// Get theme values
var fg = theme.Get(MyTheme.ForegroundColor);
var bg = theme.Get(MyTheme.BackgroundColor);
// Build output string with colors
var output = $"{fg.ToForegroundAnsi()}{bg.ToBackgroundAnsi()}{PrimaryProperty}{theme.GetResetToGlobalCodes()}";
// Use clipped rendering when a layout provider is active
if (context.CurrentLayoutProvider != null)
{
context.WriteClipped(Bounds.X, Bounds.Y, output);
}
else
{
context.Write(output);
}
}
}Theme elements allow users to customize the widget's appearance.
Location: src/Hex1b/Theming/{Name}Theme.cs
Template:
namespace Hex1b.Theming;
/// <summary>
/// Theme elements for MyWidget.
/// </summary>
public static class MyTheme
{
public static readonly Hex1bThemeElement<Hex1bColor> ForegroundColor =
new($"{nameof(MyTheme)}.{nameof(ForegroundColor)}", () => Hex1bColor.Default);
public static readonly Hex1bThemeElement<Hex1bColor> BackgroundColor =
new($"{nameof(MyTheme)}.{nameof(BackgroundColor)}", () => Hex1bColor.Default);
// For widgets with multiple states (focused, hovered, etc.)
public static readonly Hex1bThemeElement<Hex1bColor> FocusedForegroundColor =
new($"{nameof(MyTheme)}.{nameof(FocusedForegroundColor)}", () => Hex1bColor.Black);
public static readonly Hex1bThemeElement<Hex1bColor> FocusedBackgroundColor =
new($"{nameof(MyTheme)}.{nameof(FocusedBackgroundColor)}", () => Hex1bColor.White);
// For character customization
public static readonly Hex1bThemeElement<char> SomeCharacter =
new($"{nameof(MyTheme)}.{nameof(SomeCharacter)}", () => '█');
}Extension methods provide the fluent API for creating widgets.
Location: src/Hex1b/{Name}Extensions.cs
Key principles:
WidgetContext<TParent> for widgets that can be childrenTemplate:
namespace Hex1b;
using Hex1b.Widgets;
/// <summary>
/// Extension methods for creating MyWidget.
/// </summary>
public static class MyExtensions
{
/// <summary>
/// Creates a MyWidget with the specified property.
/// </summary>
public static MyWidget My<TParent>(
this WidgetContext<TParent> ctx,
string primaryProperty)
where TParent : Hex1bWidget
=> new(primaryProperty);
/// <summary>
/// Creates a MyWidget with options.
/// </summary>
public static MyWidget My<TParent>(
this WidgetContext<TParent> ctx,
string primaryProperty,
bool someOption)
where TParent : Hex1bWidget
=> new(primaryProperty) { SomeOption = someOption };
}Tests verify that the node behaves correctly. Test files use MSTest 4 (MSTest.Sdk/4.2.3 with OutputType=Exe) and import Microsoft.VisualStudio.TestTools.UnitTesting; Hex1b.Testing is global-using'd for TestSeq helpers.
Location: tests/Hex1b.Tests/{Name}NodeTests.cs
Key test scenarios:
Template:
using Hex1b;
using Hex1b.Input;
using Hex1b.Layout;
using Hex1b.Theming;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hex1b.Tests;
[TestClass]
public class MyNodeTests
{
[TestMethod]
public void Measure_ReturnsCorrectSize()
{
// Arrange
var node = new MyNode { PrimaryProperty = "Hello" };
var constraints = new Constraints(0, 100, 0, 10);
// Act
var size = node.Measure(constraints);
// Assert
Assert.AreEqual(5, size.Width); // "Hello".Length
Assert.AreEqual(1, size.Height);
}
[TestMethod]
public void PropertyChange_MarksDirty()
{
// Arrange
var node = new MyNode { PrimaryProperty = "Initial" };
node.ClearDirty(); // Simulate post-render state
// Act
node.PrimaryProperty = "Changed";
// Assert - if using property setter that marks dirty
// This depends on whether your node implements dirty tracking in setters
}
[TestMethod]
public void IsFocused_WhenSet_MarksDirty()
{
// Arrange
var node = new MyNode();
node.ClearDirty();
// Act
node.IsFocused = true;
// Assert
Assert.IsTrue(node.IsDirty);
}
}After creating all files:
# Build the library
dotnet build src/Hex1b
# Run all tests (MTP via global.json)
dotnet test
# Or run a test project as an executable
dotnet run --project tests/Hex1b.Tests/
# Or run specific tests
dotnet test --filter "MyNodeTests"For widgets that should fill available horizontal space by default:
public override Size Measure(Constraints constraints)
{
// Use all available width
var width = constraints.MaxWidth;
var height = 1;
return constraints.Constrain(new Size(width, height));
}For widgets with animation (like indeterminate progress), you need to:
MarkDirty() when animation changesHex1bApp.Invalidate() from the widget builder to trigger re-rendersFor widgets that contain children, see VStackWidget/VStackNode as examples:
Hex1bWidget[] (widget) / List<Hex1bNode> (node)ReconcileContext.ReconcileChildren() in widget's Reconcile()GetChildren() in node for focus traversalHex1bColor.Default for colors that should inherit from parentTesting widgets requires two layers: unit tests for isolated node behavior, and integration tests for real-world scenarios using Hex1bApp. Integration tests should export evidence in multiple formats for verification and documentation.
| File | Purpose |
|---|---|
tests/Hex1b.Tests/{Name}NodeTests.cs | Unit tests for node behavior |
tests/Hex1b.Tests/{Name}IntegrationTests.cs | Integration tests with Hex1bApp |
Unit tests verify isolated node behavior without running a full app.
Key scenarios to cover:
Example pattern:
[TestMethod]
public void Measure_FillsAvailableWidth()
{
var node = new ProgressNode { Value = 50, Maximum = 100 };
var size = node.Measure(new Constraints(0, 80, 0, 10));
Assert.AreEqual(80, size.Width);
Assert.AreEqual(1, size.Height);
}
[TestMethod]
public void PropertyChange_MarksDirty()
{
var node = new ProgressNode { Value = 50 };
node.ClearDirty();
// Simulate reconciliation with changed value
var widget = new ProgressWidget { Value = 75 };
widget.Reconcile(node, new ReconcileContext(...));
Assert.IsTrue(node.IsDirty);
}Integration tests spin up a real Hex1bApp and test the widget in various layout scenarios. These tests must export evidence files for verification.
Required export formats:
using Hex1b;
using Hex1b.Input;
using Hex1b.Theming;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class MyWidgetIntegrationTests : IDisposable
{
private readonly List<string> _tempFiles = new();
private string GetTempFile()
{
var path = Path.Combine(Path.GetTempPath(), $"hex1b_test_{Guid.NewGuid()}.cast");
_tempFiles.Add(path);
return path;
}
public void Dispose()
{
foreach (var file in _tempFiles)
{
try { File.Delete(file); } catch { }
}
}
}[TestMethod]
public async Task MyWidget_RendersCorrectly()
{
using var workload = new Hex1bAppWorkloadAdapter();
using var terminal = new Hex1bTerminal(workload, 60, 10);
using var app = new Hex1bApp(
ctx => ctx.VStack(v => [
v.Text("Label:"),
v.My("Hello World")
]),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
var runTask = app.RunAsync(TestContext.Current.CancellationToken);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Label:"), TimeSpan.FromSeconds(2))
.Capture("mywidget-basic") // Exports SVG, HTML, ANSI
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
await runTask;
}Test the widget in all common layout containers:
[TestMethod]
public async Task MyWidget_InBorder()
{
using var workload = new Hex1bAppWorkloadAdapter();
using var terminal = new Hex1bTerminal(workload, 60, 10);
using var app = new Hex1bApp(
ctx => ctx.Border(b => [
b.Text("Content"),
b.My("Inside border")
], title: "Panel"),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
var runTask = app.RunAsync(TestContext.Current.CancellationToken);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Panel"), TimeSpan.FromSeconds(2))
.Capture("mywidget-in-border")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
await runTask;
}
[TestMethod]
public async Task MyWidget_InHStackWithFill()
{
using var workload = new Hex1bAppWorkloadAdapter();
using var terminal = new Hex1bTerminal(workload, 80, 10);
using var app = new Hex1bApp(
ctx => ctx.HStack(h => [
h.Text("Label: "),
h.My("Content").Fill()
]),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
var runTask = app.RunAsync(TestContext.Current.CancellationToken);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Label:"), TimeSpan.FromSeconds(2))
.Capture("mywidget-hstack-fill")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
await runTask;
}
[TestMethod]
[DataRow(40)]
[DataRow(60)]
[DataRow(80)]
[DataRow(120)]
public async Task MyWidget_RespondsToTerminalWidth(int width)
{
using var workload = new Hex1bAppWorkloadAdapter();
using var terminal = new Hex1bTerminal(workload, width, 10);
using var app = new Hex1bApp(
ctx => ctx.My("Responsive content"),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
var runTask = app.RunAsync(TestContext.Current.CancellationToken);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Responsive"), TimeSpan.FromSeconds(2))
.Capture($"mywidget-width-{width}")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
await runTask;
}For widgets with animation or dynamic behavior, record asciinema sessions:
[TestMethod]
public async Task MyWidget_RecordsAnimation()
{
var tempFile = GetTempFile();
using var workload = new Hex1bAppWorkloadAdapter();
var terminalOptions = new Hex1bTerminalOptions
{
Width = 60,
Height = 10,
WorkloadAdapter = workload
};
var recorder = terminalOptions.AddAsciinemaRecorder(tempFile, new AsciinemaRecorderOptions
{
Title = "MyWidget Animation Demo",
IdleTimeLimit = 0.5f
});
using var terminal = new Hex1bTerminal(terminalOptions);
var animationValue = 0.0;
using var app = new Hex1bApp(
ctx => ctx.My($"Value: {animationValue:F1}"),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
using var cts = new CancellationTokenSource();
var runTask = app.RunAsync(cts.Token);
recorder.AddMarker("Animation Start");
// Animate for ~2 seconds
for (int i = 0; i < 40; i++)
{
animationValue = (i % 20) / 20.0;
app.Invalidate();
await Task.Delay(50, TestContext.Current.CancellationToken);
}
recorder.AddMarker("Animation End");
var snapshot = terminal.CreateSnapshot();
TestCaptureHelper.Capture(snapshot, "mywidget-animated");
await TestCaptureHelper.CaptureCastAsync(recorder, "mywidget-animation", TestContext.Current.CancellationToken);
cts.Cancel();
await runTask;
}Test how widgets respond to terminal resizing:
[TestMethod]
public async Task MyWidget_RecordsResizeScenario()
{
var tempFile = GetTempFile();
using var workload = new Hex1bAppWorkloadAdapter();
var terminalOptions = new Hex1bTerminalOptions
{
Width = 100,
Height = 10,
WorkloadAdapter = workload
};
var recorder = terminalOptions.AddAsciinemaRecorder(tempFile, new AsciinemaRecorderOptions
{
Title = "MyWidget Resize Behavior",
IdleTimeLimit = 1.0f
});
using var terminal = new Hex1bTerminal(terminalOptions);
using var app = new Hex1bApp(
ctx => ctx.My("Resize me"),
new Hex1bAppOptions { WorkloadAdapter = workload }
);
using var cts = new CancellationTokenSource();
var runTask = app.RunAsync(cts.Token);
recorder.AddMarker("Initial Size (100 cols)");
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Resize"), TimeSpan.FromSeconds(2))
.Wait(TimeSpan.FromMilliseconds(500))
.Build()
.ApplyAsync(terminal, TestContext.Current.CancellationToken);
// Resize to medium
recorder.AddMarker("Resize to 60 cols");
await ((IHex1bTerminalWorkloadFilter)recorder).OnResizeAsync(60, 10, TimeSpan.FromSeconds(1));
terminal.Resize(60, 10);
await workload.ResizeAsync(60, 10, TestContext.Current.CancellationToken);
await Task.Delay(300, TestContext.Current.CancellationToken);
TestCaptureHelper.Capture(terminal, "mywidget-resize-medium");
// Resize to narrow
recorder.AddMarker("Resize to 40 cols");
await ((IHex1bTerminalWorkloadFilter)recorder).OnResizeAsync(40, 10, TimeSpan.FromSeconds(2));
terminal.Resize(40, 10);
await workload.ResizeAsync(40, 10, TestContext.Current.CancellationToken);
await Task.Delay(300, TestContext.Current.CancellationToken);
TestCaptureHelper.Capture(terminal, "mywidget-resize-narrow");
await TestCaptureHelper.CaptureCastAsync(recorder, "mywidget-resize-demo", TestContext.Current.CancellationToken);
cts.Cancel();
await runTask;
}Verify custom themes are applied correctly:
[TestMethod]
public async Task MyWidget_RespectsCustomTheme()
{
using var workload = new Hex1bAppWorkloadAdapter();
using var terminal = new Hex1bTerminal(workload, 60, 10);
var customTheme = new Hex1bTheme("CustomTest")
.Set(MyTheme.ForegroundColor, Hex1bColor.Blue)
.Set(MyTheme.BackgroundColor, Hex1bColor.Yellow);
using var app = new Hex1bApp(
ctx => ctx.My("Themed content"),
new Hex1bAppOptions
{
WorkloadAdapter = workload,
Theme = customTheme
}
);
var runTask = app.RunAsync(TestContext.Current.CancellationToken);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Themed"), TimeSpan.FromSeconds(2))
.Capture("mywidget-custom-theme")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
await runTask;
}Every widget should have integration tests covering:
.FixedWidth() constraint.Fill() in cross-axis containerDataRow test with 40, 60, 80, 120 columnsapp.Invalidate() (if applicable)All integration tests must generate evidence files via TestCaptureHelper:
| Method | Output | Purpose |
|---|---|---|
.Capture("name") | SVG, HTML, ANSI | Static snapshot evidence |
TestCaptureHelper.Capture(terminal, "name") | SVG, HTML, ANSI | Manual capture |
TestCaptureHelper.CaptureCastAsync(recorder, "name", ct) | .cast file | Asciinema recording |
These files are attached to test results and can be viewed in CI artifacts for debugging and documentation purposes.
For complex widgets like TableWidget, consider adding visual regression tests that capture baselines and compare rendered output.
Visual regression tests use the full Hex1bTerminal stack to render widgets and capture output for comparison:
public static async Task<(string Ansi, string Text)> RenderTableAsync(
TableVisualTestCase testCase,
CancellationToken cancellationToken = default)
{
using var terminal = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithDimensions(testCase.Width, testCase.Height)
.WithHex1bApp((app, options) => ctx => BuildWidget(ctx, testCase))
.Build();
var runTask = terminal.RunAsync(cancellationToken);
// Wait for render, capture snapshot, then exit
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Expected text"), TimeSpan.FromSeconds(2))
.Wait(TimeSpan.FromMilliseconds(50))
.Build()
.ApplyAsync(terminal, cancellationToken);
using var snapshot = terminal.CreateSnapshot();
var text = snapshot.GetScreenText();
// Exit and cleanup
await new Hex1bTerminalInputSequenceBuilder()
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyAsync(terminal, cancellationToken);
await runTask;
return (ansi, text);
}Baselines are stored in tests/Hex1b.Tests/Baselines/{Widget}/ with:
.ansi files containing ANSI escape sequences (for color verification).txt files containing plain text (for structure verification)When widget rendering changes intentionally:
UPDATE_BASELINES=1 dotnet test --filter "{Widget}VisualRegressionTests"For TableWidget, the visual regression test matrix covers:
See tests/Hex1b.Tests/TableVisualRegressionTests.cs for the complete implementation.
Before considering a widget complete:
dotnet build succeedsdotnet test passesThe ProgressWidget was created following this skill. It demonstrates:
See the implementation files:
© mitchdenny, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .github/skills/widget-creator of mitchdenny/hex1b.
Open the folder on GitHubat commit 98d8766
Widget Creator next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Widget Creator this skillmitchdenny/hex1b | 178 | — | ~7.6k | Automated safety check: Pass | MIT | |
| Sync Upstreamnyaruka/phonenumbers | 1.6k | — | ~2.8k | Automated safety check: Pass | MIT | |
| Longbridge Value Investinghelsome/folio | 269 | 2 repos | ~1.2k | Automated safety check: Pass | MIT | |
| Radiology Tablehuang-sir1/radiology-skills | 1.9k | — | ~1.3k | Automated safety check: Pass | Custom licence | |
| Odoo Agency Fleet Reviewerpipe-org/mcp-odoo | 420 | — | ~699 | Automated safety check: Pass | MIT | |
| Beancount Closebex-co/beancount-io | 295 | — | ~1.4k | Automated safety check: Pass | MIT |
nyaruka/phonenumbers
Sync this Go port with a new upstream google/libphonenumber release — regenerate the embedded metadata and reconcile the ported Java logic.
helsome/folio
Value investing analysis using Graham (NCAV/net-net/defensive-investor) and Buffett (economic moat/ROE/FCF) methodologies.
huang-sir1/radiology-skills
Create/audit editable publication tables with source reconciliation; not figures or statistical inference.
erpipe-org/mcp-odoo
Review many client Odoo databases at once through odoo-mcp's cross-instance tools — fleet-wide accounting health, per-client aging, partial-failure triage — for agencies and partners managing 5–50…
bex-co/beancount-io
Close an accounting period in a Beancount ledger by reconciling each active account through beancount-reconcile, checking assertions and recurring gaps, reviewing flags, then proposing a commit with…
avansaber/erpclaw
Operates the ERPClaw self-hosted ERP in plain language: accounting, invoicing, inventory, purchasing, tax, HR, payroll and reports, treating the ERP as the single source of truth.
mitchdenny/hex1b
Guidelines for reviewing API design in the Hex1b codebase. An agent skill from mitchdenny/hex1b.
mitchdenny/hex1b
Guidelines for running and interpreting Surface API performance benchmarks.
mitchdenny/hex1b
Agent for validating Hex1b documentation against actual library behavior.
mitchdenny/hex1b
Guidelines for producing accurate and maintainable documentation for the Hex1b TUI library.
mitchdenny/hex1b
Agent for diagnosing and fixing flaky terminal UI tests in the Hex1b test suite.
mitchdenny/hex1b
Guidelines for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b.
Works with
Categories
Step-by-step guide for creating new widgets in the Hex1b TUI library. Widget Creator is an agent skill from mitchdenny/hex1b. Step-by-step guide for creating new widgets in the Hex1b TUI library.
Widget Creator fits situations like: implementing new widgets from scratch; including widget records; extension methods.
Run `npx skills add mitchdenny/hex1b --skill widget-creator -a claude-code`. Or copy the skill folder (.github/skills/widget-creator in mitchdenny/hex1b) into .claude/skills/widget-creator in your project. Claude Code loads it when a task matches its description.
Run `npx skills add mitchdenny/hex1b --skill widget-creator -a codex`. Or copy the skill folder (.github/skills/widget-creator in mitchdenny/hex1b) into .agents/skills/widget-creator in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add mitchdenny/hex1b --skill widget-creator -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/widget-creator, .gemini/skills/widget-creator, .github/skills/widget-creator and .opencode/skills/widget-creator in your project.
Going by SKILL.md and its folder, Widget Creator needs the command-line tools its instructions call (dotnet).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Widget Creator is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.6k tokens (SKILL.md is roughly 30k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Widget Creator: Sync Upstream (nyaruka/phonenumbers, 1.6k stars), Longbridge Value Investing (helsome/folio, 269 stars), Radiology Table (huang-sir1/radiology-skills, 1.9k stars) and Odoo Agency Fleet Review (erpipe-org/mcp-odoo, 420 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
mitchdenny (a GitHub user) maintains it in mitchdenny/hex1b, which has 178 GitHub stars. The repository holds 7 skills in this directory. The repository was last updated on October 2, 2026.
Source: mitchdenny/hex1b on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.