Agent skill

Widget Creator

by mitchdenny in mitchdenny/hex1b

Step-by-step guide for creating new widgets in the Hex1b TUI library.

MITAuto-check passedBusiness, Finance & HR

Install Widget Creator

skills CLI
$ npx skills add mitchdenny/hex1b --skill widget-creator -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install mitchdenny/hex1b widget-creator --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ 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-src

Use ~/.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/

Facts

Skill name
widget-creator
GitHub stars
178
Token cost
~7.6k tokens
SKILL.md length
1,207 words
Files
1
Skills in repo
7
Repo updated
First seen
Licence
MIT

At a glance

Step-by-step guide for creating new widgets in the Hex1b TUI library.

  • Works in 7 steps: Define the Widget Record → Create Event Args (if needed) → Create the Node Class → …
  • Implementing new widgets from scratch
  • SKILL.md covers Overview, Step-by-Step Process, Common Patterns and Testing Best Practices, plus 3 more sections
  • Calls dotnet

What it does

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.

When your agent uses it

  • Implementing new widgets from scratch
  • Including widget records
  • Extension methods

Example prompts

  • “/widget-creator”

Workflow steps

7 steps, taken from the step headings in SKILL.md.

  1. Define the Widget Record
  2. Create Event Args (if needed)
  3. Create the Node Class
  4. Create Theme Elements
  5. Create Extension Methods
  6. Write Unit Tests
  7. Build and Test

What it can do on your machine

Read from SKILL.md and the folder at commit 98d8766. It shows what the files ask for, not the result of running them.

  • Tool permissions

    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.

  • Runs code

    Shell commands in SKILL.md call:

    • dotnet

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

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.

Always · name and description, kept in context so the agent knows when to use it
~55
When it runs · the whole SKILL.md, loaded when a task matches
~7.6k

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.

Safety

Auto-check passed

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.

SKILL.md

The full file from mitchdenny/hex1b at commit 98d8766, republished under its MIT licence (© mitchdenny). 1,207 words, ~7,570 tokens.

Download SKILL.mdSave it as .claude/skills/widget-creator/SKILL.md (or your agent's skills folder).
name
widget-creator
description
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.

Widget Creator Skill

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.

Overview

Creating a widget in Hex1b involves several coordinated files:

FilePurpose
src/Hex1b/Widgets/{Name}Widget.csImmutable widget record (describes what to render)
src/Hex1b/Nodes/{Name}Node.csMutable node class (manages state, renders)
src/Hex1b/{Name}Extensions.csFluent API extension methods
src/Hex1b/Theming/{Name}Theme.csTheme elements (colors, characters)
tests/Hex1b.Tests/{Name}NodeTests.csUnit tests

Step-by-Step Process

Step 1: Define the Widget Record

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:

  • Use primary constructor parameters for required data
  • Use internal properties with init for optional configuration
  • Event handlers use Func<TEventArgs, Task>? pattern
  • Provide sync and async overloads for event handlers using this with { } pattern
  • Implement Reconcile() to create/update the corresponding node
  • Implement GetExpectedNodeType() to return the node type

Template:

csharp
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);
}
Step 2: Create Event Args (if needed)

If your widget has event handlers, create a typed event args class.

Location: src/Hex1b/Events/{Name}EventArgs.cs

Template:

csharp
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;
    }
}
Step 3: Create the Node Class

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:

  • Properties are mutable (set from widget during reconciliation)
  • Track state that must survive re-renders (focus, cursor position, etc.)
  • Call MarkDirty() when internal state changes
  • Implement Measure() to calculate size
  • Implement Render() to draw to terminal
  • Override IsFocusable if the widget can receive focus
  • Override ConfigureDefaultBindings() for keyboard/mouse handling

Lift 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(...). See TextBoxWidget / TextBoxState, EditorWidget / EditorState, NavigatorWidget / NavigatorState, CheckboxWidget / CheckboxState for 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:

csharp
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);
        }
    }
}
Step 4: Create Theme Elements

Theme elements allow users to customize the widget's appearance.

Location: src/Hex1b/Theming/{Name}Theme.cs

Template:

csharp
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)}", () => '█');
}
Step 5: Create Extension Methods

Extension methods provide the fluent API for creating widgets.

Location: src/Hex1b/{Name}Extensions.cs

Key principles:

  • Extend WidgetContext<TParent> for widgets that can be children
  • Provide overloads for common patterns
  • Use XML documentation for IntelliSense

Template:

csharp
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 };
}
Step 6: Write Unit Tests

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:

  • Measure returns correct size
  • Render outputs expected content
  • Input handling works correctly
  • Property changes mark node dirty
  • Focus state changes work

Template:

csharp
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);
    }
}
Step 7: Build and Test

After creating all files:

bash
# 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"

Common Patterns

Fill Width (Horizontal Fill)

For widgets that should fill available horizontal space by default:

csharp
public override Size Measure(Constraints constraints)
{
    // Use all available width
    var width = constraints.MaxWidth;
    var height = 1;
    return constraints.Constrain(new Size(width, height));
}
Animation / Indeterminate State

For widgets with animation (like indeterminate progress), you need to:

  1. Track animation state in the node
  2. Call MarkDirty() when animation changes
  3. Use Hex1bApp.Invalidate() from the widget builder to trigger re-renders
Container Widgets

For widgets that contain children, see VStackWidget/VStackNode as examples:

  • Store children in Hex1bWidget[] (widget) / List<Hex1bNode> (node)
  • Use ReconcileContext.ReconcileChildren() in widget's Reconcile()
  • Implement GetChildren() in node for focus traversal
Theming Best Practices
  1. Use Hex1bColor.Default for colors that should inherit from parent
  2. Provide focused/hovered variants for interactive widgets
  3. Use characters for customizable borders/bullets so users can theme them

Testing Best Practices

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

Test File Organization
FilePurpose
tests/Hex1b.Tests/{Name}NodeTests.csUnit tests for node behavior
tests/Hex1b.Tests/{Name}IntegrationTests.csIntegration tests with Hex1bApp
Unit Tests (NodeTests)

Unit tests verify isolated node behavior without running a full app.

Key scenarios to cover:

  1. Measure behavior - Size calculations for various constraints
  2. Render output - Correct ANSI sequences and text
  3. Property dirty tracking - Changes trigger re-render
  4. Input handling - Key/mouse events processed correctly
  5. Focus state - Focus changes mark dirty and render differently
  6. Reconciliation - Widget updates preserve/update node state

Example pattern:

csharp
[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);
}
Show full SKILL.md (500 more words)Show less
Integration Tests (IntegrationTests)

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:

  • SVG - Vector graphics for documentation
  • HTML - Styled HTML output
  • ANSI - Raw terminal sequences
  • Asciinema (.cast) - Animated recordings for dynamic behavior
Test Infrastructure Setup
csharp
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 { }
        }
    }
}
Basic Integration Test Pattern
csharp
[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;
}
Layout Scenario Tests

Test the widget in all common layout containers:

csharp
[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;
}
Asciinema Recording Tests

For widgets with animation or dynamic behavior, record asciinema sessions:

csharp
[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;
}
Resize Scenario Tests

Test how widgets respond to terminal resizing:

csharp
[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;
}
Theming Tests

Verify custom themes are applied correctly:

csharp
[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;
}
Integration Test Checklist

Every widget should have integration tests covering:

  • Basic rendering - Widget appears correctly in simple VStack
  • In Border - Widget inside a Border container
  • In HStack - Widget with label in HStack
  • Multiple instances - Several widgets in a VStack
  • Fixed width - Widget with .FixedWidth() constraint
  • Fill behavior - Widget with .Fill() in cross-axis container
  • Various terminal widths - MSTest DataRow test with 40, 60, 80, 120 columns
  • Custom theme - Widget with custom theme elements
  • Dynamic updates - Widget responding to app.Invalidate() (if applicable)
  • Animation recording - Asciinema recording for animated states (if applicable)
  • Resize behavior - Recording of terminal resize handling (if fills space)
Export Evidence Requirements

All integration tests must generate evidence files via TestCaptureHelper:

MethodOutputPurpose
.Capture("name")SVG, HTML, ANSIStatic snapshot evidence
TestCaptureHelper.Capture(terminal, "name")SVG, HTML, ANSIManual capture
TestCaptureHelper.CaptureCastAsync(recorder, "name", ct).cast fileAsciinema recording

These files are attached to test results and can be viewed in CI artifacts for debugging and documentation purposes.

Visual Regression Testing

For complex widgets like TableWidget, consider adding visual regression tests that capture baselines and compare rendered output.

Baseline Test Infrastructure

Visual regression tests use the full Hex1bTerminal stack to render widgets and capture output for comparison:

csharp
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);
}
Baseline Storage

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)
Updating Baselines

When widget rendering changes intentionally:

bash
UPDATE_BASELINES=1 dotnet test --filter "{Widget}VisualRegressionTests"
Test Matrix Example

For TableWidget, the visual regression test matrix covers:

  • Data sizes: 0, 1, 5, 50, 1000 rows
  • Render modes: Compact, Full
  • Terminal sizes: 80×24, 160×48
  • Selection states: None, some, all selected
  • Focus states: Row focus, table focus indicator

See tests/Hex1b.Tests/TableVisualRegressionTests.cs for the complete implementation.

Checklist

Before considering a widget complete:

  • Widget record with all configuration properties
  • Event args class (if widget has events)
  • Node class with Measure, Render, and input handling
  • Theme class with customizable elements
  • Extension methods for fluent API
  • Unit tests for core node functionality
  • Integration tests with exhaustive layout scenarios
  • Export evidence (SVG, HTML, ANSI, Asciinema) from integration tests
  • dotnet build succeeds
  • dotnet test passes

Example: ProgressWidget

The ProgressWidget was created following this skill. It demonstrates:

  • Determinate mode: Shows progress from min to max value
  • Indeterminate mode: Animated spinner for unknown completion
  • Fill width: Uses all available horizontal space by default
  • Theming: Customizable fill and track characters
  • Comprehensive tests: Unit tests and integration tests with full export evidence

See the implementation files:

  • src/Hex1b/Widgets/ProgressWidget.cs
  • src/Hex1b/Nodes/ProgressNode.cs
  • src/Hex1b/ProgressExtensions.cs
  • src/Hex1b/Theming/ProgressTheme.cs
  • tests/Hex1b.Tests/ProgressNodeTests.cs (unit tests)
  • tests/Hex1b.Tests/ProgressIntegrationTests.cs (integration tests)

© mitchdenny, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .github/skills/widget-creator of mitchdenny/hex1b.

Open the folder on GitHubat commit 98d8766

Compare with similar skills

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.

Widget Creator compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Widget Creator this skillmitchdenny/hex1b178—~7.6kAutomated safety check: PassMIT
Sync Upstreamnyaruka/phonenumbers1.6k—~2.8kAutomated safety check: PassMIT
Longbridge Value Investinghelsome/folio2692 repos~1.2kAutomated safety check: PassMIT
Radiology Tablehuang-sir1/radiology-skills1.9k—~1.3kAutomated safety check: PassCustom licence
Odoo Agency Fleet Reviewerpipe-org/mcp-odoo420—~699Automated safety check: PassMIT
Beancount Closebex-co/beancount-io295—~1.4kAutomated safety check: PassMIT

Similar skills

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

    1.6k GitHub stars~2.8k tokensUpdated 6 days ago
    Business, Finance & HRAuto-check passed
  • Value investing analysis using Graham (NCAV/net-net/defensive-investor) and Buffett (economic moat/ROE/FCF) methodologies.

    269 GitHub starsUsed in 2 repos~1.2k tokens
    Business, Finance & HRAuto-check passed
  • Radiology Table

    huang-sir1/radiology-skills

    Create/audit editable publication tables with source reconciliation; not figures or statistical inference.

    1.9k GitHub stars~1.3k tokensUpdated 17 days ago
    Business, Finance & HRAuto-check passed
  • Odoo Agency Fleet Review

    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…

    420 GitHub stars~699 tokensUpdated 1 mo ago
    Business, Finance & HRAuto-check passed
  • Beancount Close

    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…

    295 GitHub stars~1.4k tokensUpdated yesterday
    Business, Finance & HRAuto-check passed
  • ERPClaw ERP Controller

    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.

    114 GitHub stars~15k tokensUpdated 2 days ago
    Business, Finance & HRAuto-check passed

More from mitchdenny/hex1b

  • API Reviewer

    mitchdenny/hex1b

    Guidelines for reviewing API design in the Hex1b codebase. An agent skill from mitchdenny/hex1b.

    178 GitHub stars~4k tokensUpdated 6 days ago
    Auto-check passed
  • Surface Benchmarker

    mitchdenny/hex1b

    Guidelines for running and interpreting Surface API performance benchmarks.

    178 GitHub stars~3.1k tokensUpdated 6 days ago
    Auto-check passed
  • Doc Tester

    mitchdenny/hex1b

    Agent for validating Hex1b documentation against actual library behavior.

    178 GitHub stars~9.5k tokensUpdated 6 days ago
    Auto-check passed
  • Doc Writer

    mitchdenny/hex1b

    Guidelines for producing accurate and maintainable documentation for the Hex1b TUI library.

    178 GitHub stars~8.7k tokensUpdated 6 days ago
    Auto-check passed
  • Test Fixer

    mitchdenny/hex1b

    Agent for diagnosing and fixing flaky terminal UI tests in the Hex1b test suite.

    178 GitHub stars~6.5k tokensUpdated 6 days ago
    Auto-check passed
  • Writing Unit Tests

    mitchdenny/hex1b

    Guidelines for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b.

    178 GitHub stars~6.6k tokensUpdated 6 days ago
    Auto-check passed

Works with

Questions about Widget Creator

What does Widget Creator do?

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.

When should I use Widget Creator?

Widget Creator fits situations like: implementing new widgets from scratch; including widget records; extension methods.

How do I install Widget Creator in Claude Code?

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.

How do I install Widget Creator in Codex?

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.

Can I use Widget Creator in Cursor, Gemini CLI or GitHub Copilot?

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.

What does Widget Creator need to run?

Going by SKILL.md and its folder, Widget Creator needs the command-line tools its instructions call (dotnet).

Does Widget Creator access the network?

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.

Is Widget Creator safe to install?

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.

What licence does Widget Creator use?

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.

How many tokens does Widget Creator use?

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.

What are the alternatives to Widget Creator?

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.

Who maintains Widget Creator?

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.