Skip to content

TerminalWidget

Embed child terminal sessions within your TUI application.

Overview

The TerminalWidget displays the output of an embedded terminal session, such as a bash shell, PowerShell, or any PTY-based process. This enables building terminal multiplexers, IDE-like environments, or any application that needs to host child terminals.

Key Concept

The TerminalWidget connects to a TerminalWidgetHandle which provides the screen buffer from a running terminal session. The handle is created using Hex1bTerminalBuilder.WithTerminalWidget().

Live Demo

This interactive demo shows multiple embedded terminals with menu-driven switching and restart functionality. Try typing help for available commands, or colors for a color test:

csharp

Diagnostic Shell

The live demo uses the built-in diagnostic shell which simulates a terminal environment without requiring PTY infrastructure. For real applications, you'd use .WithPtyProcess("bash") or similar.

Basic Usage

Create an embedded terminal and display it in your TUI:

csharp
using Hex1b;

// Create a terminal with an embedded bash session
var bashTerminal = Hex1bTerminal.CreateBuilder()
    .WithPtyProcess("bash", "--norc")
    .WithTerminalWidget(out var bashHandle)
    .Build();

// Start the child terminal in the background
_ = bashTerminal.RunAsync();

// Create the main TUI app with the embedded terminal
await using var displayTerminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((app, options) => ctx => 
        ctx.Border(
            ctx.Terminal(bashHandle),
            title: "Embedded Bash Terminal"
        ))
    .Build();

await displayTerminal.RunAsync();

The key steps are:

  1. Create the child terminal using Hex1bTerminal.CreateBuilder() with .WithTerminalWidget(out var handle)
  2. Start the child terminal with RunAsync() in the background
  3. Display the terminal using ctx.Terminal(handle) in your widget tree

Multiple Terminals

You can embed multiple terminal sessions and arrange them using layout widgets:

csharp
using Hex1b;
using Hex1b.Input;

// Create multiple embedded terminals
var bash = Hex1bTerminal.CreateBuilder()
    .WithPtyProcess("bash", "--norc")
    .WithTerminalWidget(out var bashHandle)
    .Build();

var pwsh = Hex1bTerminal.CreateBuilder()
    .WithPtyProcess("pwsh", "-NoProfile", "-NoLogo")
    .WithTerminalWidget(out var pwshHandle)
    .Build();

// Start both terminals
_ = bash.RunAsync();
_ = pwsh.RunAsync();

// Display side-by-side using HStack
await using var displayTerminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((app, options) => ctx => 
        ctx.HStack(h => [
            h.Border(
                h.Terminal(bashHandle),
                title: "Bash"
            ).Fill(),
            h.Border(
                h.Terminal(pwshHandle),
                title: "PowerShell"
            ).Fill()
        ]))
    .Build();

await displayTerminal.RunAsync();

Fallback Widget

When a terminal process exits, you can display a fallback widget using WhenNotRunning():

csharp
using Hex1b;

var terminal = Hex1bTerminal.CreateBuilder()
    .WithPtyProcess("bash", "--norc")
    .WithTerminalWidget(out var bashHandle)
    .Build();

Hex1bApp? app = null;

void RestartTerminal()
{
    bashHandle.Reset();
    _ = terminal.RunAsync();
    app?.Invalidate();
}

_ = terminal.RunAsync();

await using var displayTerminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((hex1bApp, options) =>
    {
        app = hex1bApp;
        return ctx => ctx.Border(
            ctx.Terminal(bashHandle)
                .WhenNotRunning(args => ctx.VStack(v => [
                    v.Text(""),
                    v.Align(Alignment.Center, v.VStack(center => [
                        center.Text($"Terminal exited with code {args.ExitCode ?? 0}"),
                        center.Text(""),
                        center.HStack(buttons => [
                            buttons.Button("Restart").OnClick(_ => RestartTerminal()),
                            buttons.Text("  "),
                            buttons.Button("Close").OnClick(_ => hex1bApp.RequestStop())
                        ])
                    ]))
                ])),
            title: "Terminal with Fallback"
        );
    })
    .Build();

await displayTerminal.RunAsync();

The WhenNotRunning callback receives a TerminalNotRunningArgs object containing:

PropertyTypeDescription
HandleTerminalWidgetHandleThe terminal handle
StateTerminalStateCurrent state (NotStarted or Completed)
ExitCodeint?Exit code if completed, null otherwise

Terminal States

The TerminalWidgetHandle tracks the lifecycle of the child process:

csharp
using Hex1b;

// Handle terminal lifecycle states
bashHandle.StateChanged += state =>
{
    switch (state)
    {
        case TerminalState.NotStarted:
            Console.WriteLine("Terminal not started yet");
            break;
        case TerminalState.Running:
            Console.WriteLine("Terminal is running");
            break;
        case TerminalState.Completed:
            Console.WriteLine($"Terminal exited with code {bashHandle.ExitCode}");
            break;
    }
};
StateDescription
NotStartedTerminal created but not yet started
RunningTerminal process is actively running
CompletedTerminal process has exited

Window Title Support

Child processes can set the terminal title using OSC escape sequences. Subscribe to title changes:

csharp
using Hex1b;

// Subscribe to title changes from the child terminal
bashHandle.WindowTitleChanged += title =>
{
    // Update your UI to reflect the new title
    app.Invalidate();
};

// Use the title in your widget tree
ctx.Border(
    ctx.Terminal(bashHandle),
    title: bashHandle.WindowTitle ?? "Terminal"
);

The handle provides:

  • WindowTitle - Current window title (OSC 0/2)
  • IconName - Current icon name (OSC 0/1)
  • WindowTitleChanged event - Fired when title changes
  • IconNameChanged event - Fired when icon name changes

Input Handling

When a TerminalWidget has focus:

  • Keyboard input is automatically forwarded to the child process
  • Mouse events are forwarded if the child process has enabled mouse tracking
  • Focus is captured while the terminal is running

Focus Behavior

When the child terminal exits, focus is released and can move to the fallback widget's interactive elements (like buttons).

API Reference

TerminalWidget Record

csharp
public sealed record TerminalWidget(TerminalWidgetHandle Handle) : Hex1bWidget

Extension Methods

MethodDescription
ctx.Terminal(handle)Creates a TerminalWidget bound to the handle
.WhenNotRunning(builder)Sets a fallback widget for when the terminal is not running

TerminalWidgetHandle Properties

PropertyTypeDescription
WidthintCurrent width in columns
HeightintCurrent height in rows
StateTerminalStateCurrent lifecycle state
ExitCodeint?Exit code if completed
IsRunningboolWhether the terminal is currently running
WindowTitlestringCurrent window title from OSC sequences
CursorXintCurrent cursor X position (0-based)
CursorYintCurrent cursor Y position (0-based)
CursorVisibleboolWhether cursor is visible
MouseTrackingEnabledboolWhether child has enabled mouse tracking

TerminalWidgetHandle Methods

MethodDescription
SendEventAsync(evt)Send a key or mouse event to the child process
Resize(width, height)Resize the terminal buffer
Reset()Reset state to NotStarted (for restarting)
GetScreenBuffer()Get a copy of the current screen buffer
  • Terminal Emulator Guide — Learn about Hex1b's terminal emulation capabilities
  • Stacks — Layout multiple terminals with HStack/VStack
  • Border — Add borders and titles to terminal panes

Released under the MIT License.