Hex1b

repository·main·Indexed 41 days ago

https://github.com/mitchdenny/hex1b

.NET library for building rich, interactive terminal user interfaces (TUI) using a declarative, React-inspired API. It features a constraint-based layout system with SizeHint, a widget/node separation pattern for efficient reconciliation, and built-in widgets such as TextBlockWidget, ButtonWidget, and SplitterWidget.

Tokens
221.2K
Snippets
568
Records
815
Agent score
51%

What's inside Hex1b

  1. Overview of Hex1b capabilities

    main

    Hex1b is a multi-purpose terminal framework providing the following core capabilities:

    • Terminal User Interfaces (TUI): Build interactive, declarative dashboards and dev tools using a React-inspired API.
    • Terminal Emulator: Embed a programmable terminal emulator within .NET applications to host shells and execute commands.
    • Automation & Testing: Programmatically test terminal applications using input sequencing, pattern matching, and CI/CD integration.
    • MCP Server: Expose terminal sessions to AI agents via the Model Context Protocol (MCP) for LLM-driven automation.
    • CLI Tool: Use dotnet hex1b to debug, capture, and script terminal sessions from the command line.
  2. Overview of the Hex1b terminal application stack

    main

    Hex1b is a comprehensive .NET terminal application stack designed for several use cases:

    • Building rich text-based user interfaces (TUI): Create interactive terminal applications.
    • Programmable terminal emulation: Use Hex1b as a programmable terminal emulator.
    • Terminal application testing: Test the behavior and output of terminal-based software.
    • AI Agent integration: Integrate AI agents directly with terminal sessions.
  3. Build Terminal User Interfaces with Hex1b

    main
    Hex1b allows you to build rich, interactive terminal user interfaces (TUIs) using a declarative, React-inspired API. Instead of manually managing cursor positioning or screen updates, you describe the UI structure using widgets, and Hex1b handles automatic diffing (updating only changed regions), constraint-based layouts, and state synchronization.
  4. Explore the Hex1b API namespaces

    main

    The Hex1b library is organized into several functional namespaces. Use these to locate specific functionality for building your TUI:

    • Hex1b: Core application types, extension methods, and the render context.
    • Hex1b.Widgets: Widget records used to describe UI components.
    • Hex1b.Nodes: Mutable node classes that manage state and rendering logic.
    • Hex1b.Layout: Layout primitives including Constraints, Size, Rect, and SizeHint.
    • Hex1b.Input: Input handling, key bindings, and input routing.
    • Hex1b.Events: Event argument types for widget interactions.
    • Hex1b.Theming: The theme system for customizing colors and characters.
    • Hex1b.Tokens: Design tokens for consistent theming.
    • Hex1b.Animation: Animation timers and utilities.
    • Hex1b.Automation: Terminal automation, input sequences, and snapshot testing.
    • Hex1b.Surfaces: Low-level surface rendering and cell manipulation.
  5. Overview of testing Hex1b applications

    main

    Testing Hex1b applications is achieved through a combination of a virtual terminal, an input sequence builder, and an automator. This approach allows you to simulate user interactions in a headless environment and verify application state.

    Core Testing Components

    • Hex1bTerminal(width, height): A virtual terminal used to capture screen output and provide a workload adapter.
    • terminal.WorkloadAdapter: The bridge used to connect your Hex1bApp to the virtual terminal.
    • Hex1bTerminalInputSequenceBuilder: A fluent API used to construct complex sequences of keyboard and mouse inputs.
    • Hex1bTerminalAutomator: An imperative async API designed for complex tests requiring rich error diagnostics.
    • terminal.GetScreenText(): An API to retrieve the current screen content as a string (automatically flushes the buffer).
    // The general testing pattern
    using var terminal = new Hex1bTerminal(80, 24);
    using var app = new Hex1bApp(/* ... */);
    
    var sequence = new Hex1bInputSequenceBuilder()
        .Type("Hello")
        .Enter()
        .Build();
    
    await sequence.ApplyAsync(terminal);
    
    StringAssert.Contains(terminal.GetScreenText(), "Hello");
  6. Manage Shell Marks and Bookmarks

    main

    The terminal manages two types of anchors in the scroll history:

    1. Shell Marks: These are command ticks generated by the shell/application using OSC 133 output. They represent specific points in the command history (Prompt, Input, Executing, or Finished). Command marks require the shell to explicitly provide them; they are not automatically inferred.
    2. Bookmarks: User-defined anchors that point to a specific row. Unlike shell marks, bookmarks are per-view and are not transferred between sessions or relayed via HMP1.
    • scrollToMarker: Use this API to jump to a specific mark. It supports producer reflow. If an anchor is unavailable, evicted, or expired, the jump will be rejected rather than jumping to an unrelated row.
    • Marks (N) Menu: A menu in the window title bar that lists retained shell marks and bookmarks with clickable row locations and exit statuses.
  7. Anchor floats to nested widgets or other floats

    main

    Anchoring to Nested Widgets

    The anchor does not need to be a direct child of the container. It can be nested inside layout wrappers like Center or Padding. The float will position itself relative to the anchor's actual rendered bounds, not the wrapper's bounds.

    Float-to-Float Anchoring

    Floats can anchor to other floated widgets within the same container.

    CRITICAL: The anchor float must be declared before the dependent float in the children array, as floats are arranged in declaration order.

    // Example: Float-to-Float anchoring
    ctx.VStack(v =>
    {
        var menu = v.Border(b => [b.Text("Menu")]).Title("Menu");
        var tooltip = v.Text("Tooltip text");
        return [
            v.Float(menu).Absolute(5, 3),
            v.Float(tooltip).ExtendRight(menu).AlignTop(menu),
        ];
    })
  8. Understand Sixel background handling

    main

    Hex1b handles unpainted pixels based on the P2 parameter in the DCS introducer:

    • P2=1 (Transparent): Unpainted pixels preserve the underlying graphics or text result.
    • P2=0 or 2 (Opaque): Unpainted pixels are filled across the final logical extent with the terminal background color captured at the moment the graphic was created.

    Key details:

    • The captured background color is fixed at creation time; subsequent SGR background changes do not affect existing graphics.
    • If the terminal background is unset, the fill defaults to deterministic black (#000000FF).
    • This behavior matches foot and xterm.js rather than xterm or WezTerm (which use register 0).
  9. Create Breakdown, Donut, and Pie charts

    main

    Breakdown Chart

    BreakdownChartWidget<T> displays a single proportional segmented bar showing how parts contribute to a whole. It only supports a single series. It is best paired with a Legend widget.

    Donut and Pie Charts

    DonutChartWidget<T> displays proportional data as colored arc segments around a ring.

    • To create a Pie Chart: Call .HoleSize(0.0) to fill the center.
    • Hole Size Control: Use .HoleSize(double) to set the inner radius as a fraction of the outer radius (e.g., 0.5 for a classic donut, 0.8 for a thin ring).

    Pair these with a Legend to show labels and percentages.

  10. Configure child sizing in HStackWidget

    main

    HStackWidget supports three sizing modes for its children:

    1. Content Sizing (Default): Children take up exactly the width required by their content.
    2. Fill Sizing: Using .Fill() allows a child to expand and consume remaining available space. If multiple children use .Fill(), the space is distributed evenly among them.
    3. Fixed Sizing: Using .FixedWidth(int) forces a child to take a specific width in cells.

    To distribute space proportionally among multiple filling children, use .FillWidth(int) (Weighted Fill).

    // Content Sizing
    ctx.HStack(h => [
        h.Text("Short"),
        h.Text("Longer Text")
    ]);
    
    // Fill Sizing (Even distribution)
    ctx.HStack(h => [
        h.Text("Left").Fill(),
        h.Text("Right").Fill()
    ]);
    
    // Fixed Sizing
    ctx.HStack(h => [
        h.Text("Label").FixedWidth(20),
        h.TextBox(value, onChange).Fill()
    ]);
    
    // Weighted Fill (Proportional distribution)
    ctx.HStack(h => [
        h.Panel(sidebar).FillWidth(1), // Gets 1/3 of space
        h.Panel(main).FillWidth(2)     // Gets 2/3 of space
    ]);
  11. Distinguish between Terminal Builder and Widget Composition

    main

    Hex1b uses two distinct fluent surfaces to separate host configuration from UI description. It is critical to use the correct naming convention to ensure code readability and correct behavior.

    1. Terminal Builder (Hex1bTerminalBuilder)

    Used to wire up the host process (terminal plumbing, adapters, recording, mouse, etc.). This is configured once at startup.

    • Naming Convention: All methods must use the With* prefix.
    • Behavior: Mutates a builder configuration object and returns the same builder for chaining.
    • Example: terminal.WithMouse().WithHex1bApp(...)

    2. Widget Composition (WidgetContext<T> and widget instances)

    Used to describe the UI tree on every render pass. Widgets are immutable records.

    • Naming Convention: Use bare verb-nouns (no With* prefix).
    • Behavior: Returns a new widget value (or a new instance via an extension method).
    • Example: context.Border(b => [...]).Title("Hi")

    Rule of thumb: If you are working with a widget and want to set a property, do not use WithTitle(...); use Title(...) instead.

    // Correct Terminal Builder usage
    terminal.WithMouse().WithHex1bApp(...);
    
    // Correct Widget usage
    context.Border(b => [ b.Text("Hello") ]).Title("My Border");
  12. Format client input messages in HWT1

    main

    All client messages sent to the server must be complete UTF-8 JSON objects. Each message has a maximum size of 65,536 bytes (including JSON syntax and escaping).

    Constraints:

    • Required properties must match their specified types; null is not a valid substitute for a required string.
    • Unknown command types are rejected.
    • For shift, alt, and ctrl modifiers, use optional booleans (defaulting to false). Non-boolean values are invalid.
    • The meta modifier is ignored by the core protocol.
    • Multi-code-point characters or IME/composition commits should be sent via input messages, not key messages.