Terminal Emulator
Why a Terminal Emulator with an API?
When automating or testing terminal applications, many tools rely on simple stdin/stdout redirection. While this works for basic scenarios, it quickly falls short for real-world terminal applications:
- No screen state: Stdin/stdout gives you a stream of bytes, not a structured view of what's on screen. You can't easily ask "what text is at row 5, column 10?" or "is the cursor blinking?"
- No timing context: Terminal applications use escape sequences to position text, change colors, and manage the display. Raw byte streams don't tell you when a "frame" is complete.
- No interactivity: Testing a TUI application means simulating user input and verifying visual output—you need something that understands terminal semantics.
- No embedability: If you want to host a shell or another TUI inside your application (think VS Code's integrated terminal), you need a terminal emulator that maintains screen state in-process.
Hex1b's Hex1bTerminal is a headless terminal emulator—it processes ANSI/VT escape sequences and maintains an in-memory screen buffer, but doesn't render to any display by itself. This separation is intentional:
Headless by design:
Hex1bTerminalowns the terminal state (screen buffer, cursor, attributes). Presentation adapters bridge that state to whatever UI you're targeting—the local console, a web browser, a GUI window, or nothing at all (for testing).
This architecture enables:
- Automation: Programmatically read screen content, wait for specific text, inject keystrokes
- Testing: Run TUI applications in CI/CD without a real terminal
- Embedding: Host shells, editors, or other terminal programs inside your application
- Remote terminals: Stream terminal state to web clients or other processes
Tutorial: Build a Web Terminal
For a hands-on walkthrough building a multi-terminal web app with WebSocket streaming and xterm.js, see Using the Emulator.
Key Features
Full VT/ANSI Support
Hex1b's terminal emulator supports the full range of terminal escape sequences:
- Text formatting — Colors (16, 256, and 24-bit), bold, italic, underline, strikethrough
- Cursor control — Positioning, visibility, shape changes
- Screen management — Scrolling regions, alternate screen buffer
- Mouse support — Click, drag, and scroll event reporting
- Unicode — Full Unicode support including emoji and complex scripts
Child Process Integration
Run any command with proper PTY (pseudo-terminal) support:
// Start an interactive shell with PTY
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithPtyProcess("bash", "-i")
.Build();
await terminal.RunAsync();// Or run a command without PTY (for build tools, scripts)
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithProcess("dotnet", "build")
.WithHeadless()
.Build();
await terminal.RunAsync();Docker Container Integration
Run commands inside isolated Docker containers using WithDockerContainer(). This wraps WithPtyProcess under the hood, so all terminal features (input sequences, pattern searching, recording) work unchanged:
// Start a container with the default .NET SDK image
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithDockerContainer()
.Build();
await terminal.RunAsync();// Configure the container with a specific image and environment
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithDockerContainer(c =>
{
c.Image = "ubuntu:24.04";
c.Environment["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
c.Volumes.Add("/host/data:/container/data:ro");
c.WorkingDirectory = "/app";
})
.Build();
await terminal.RunAsync();// Build from a Dockerfile (automatically skips rebuild if unchanged)
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithDockerContainer(c =>
{
c.DockerfilePath = "./test-env/Dockerfile";
c.BuildArgs["SDK_VERSION"] = "10.0";
})
.Build();
await terminal.RunAsync();The container runs interactively with docker run -it and is automatically removed on exit (--rm). Key options:
| Option | Default | Description |
|---|---|---|
Image | mcr.microsoft.com/dotnet/sdk:10.0 | Docker image to use |
DockerfilePath | null | Build from a Dockerfile instead of pulling an image |
Environment | {} | Environment variables (-e) |
Volumes | [] | Volume mounts (-v) |
MountDockerSocket | false | Mount /var/run/docker.sock for Docker-in-Docker |
Name | auto-generated | Explicit container name |
Shell | /bin/bash | Shell to run inside the container |
ShellArgs | ["--norc"] | Arguments passed to the shell |
AutoRemove | true | Remove container on exit (--rm) |
Network | null | Docker network to connect to |
When to use WithDockerContainer vs WithPtyProcess
Use WithDockerContainer when you need an isolated, reproducible environment—particularly for testing or running untrusted code. Use WithPtyProcess when you want to run a local command directly. Under the hood, WithDockerContainer simply builds the docker run -it arguments and calls WithPtyProcess("docker", args).
Programmatic Control
Read and write to the terminal programmatically:
// Send input to the terminal
terminal.SendInput("ls -la\n");
// Read the current screen content
var screen = terminal.GetScreenContent();
// Wait for specific output
await terminal.WaitForTextAsync("$");Architecture
The terminal emulator consists of several components:
| Component | Purpose |
|---|---|
Hex1bTerminal | Headless terminal emulator — maintains screen state |
Hex1bTerminal.CreateBuilder() | Fluent configuration builder |
IHex1bTerminalPresentationAdapter | Bridges terminal state to a display |
IHex1bTerminalWorkloadAdapter | Manages what runs inside the terminal |
Presentation Adapters
Since Hex1bTerminal is headless, you need a presentation adapter to display its content:
ConsolePresentationAdapter— Render toSystem.Console(the default)HeadlessPresentationAdapter— No rendering (for testing/automation)WebSocketPresentationAdapter— Stream to WebSocket clients (xterm.js, etc.)- Custom adapters — Implement your own for GUIs, web, etc.
See Presentation Adapters for details.
Use Cases
Embedding in a TUI
Combine the terminal emulator with Hex1b's TUI framework:
var terminal = new Hex1bTerminalBuilder()
.WithSize(80, 24)
.Build();
var app = new Hex1bApp(ctx =>
ctx.VStack(v => [
v.Text("My Terminal App"),
v.Terminal(terminal).Fill(),
v.InfoBar("Ctrl+D: Exit")
])
);Building Dev Tools
Create development tools with integrated terminals:
var app = new Hex1bApp(ctx =>
ctx.Splitter(
left: ctx.Border(b => [
b.Text("Files"),
b.List(files)
]),
right: ctx.Terminal(terminal)
)
);Automation & Testing
See the Automation & Testing guide for using the terminal emulator in testing scenarios.
Terminal Reflow
When a terminal is resized, soft-wrapped lines can be re-wrapped to fit the new width—a feature called reflow. Hex1b supports pluggable reflow strategies that match the behavior of different terminal emulators.
How It Works
Hex1b tracks soft wrapping at the cell level. When a character is written past the last column and the cursor wraps to the next line, the wrap-point cell is tagged with the SoftWrap attribute. Hard wraps (explicit \r\n) are not tagged.
On resize, the reflow engine:
- Collects all rows (scrollback + screen)
- Groups them into logical lines using
SoftWrapflags - Re-wraps each logical line to the new width
- Distributes rows back to screen and scrollback
Enabling Reflow
Reflow is disabled by default to preserve the traditional crop-and-extend resize behavior. Enable it by calling WithReflow() on the terminal builder:
// Auto-detect strategy based on TERM_PROGRAM
var terminal = Hex1bTerminal.CreateBuilder()
.WithPtyProcess("bash")
.WithReflow()
.Build();
// Explicit strategy for testing
var terminal = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithDimensions(80, 24)
.WithReflow(AlacrittyReflowStrategy.Instance)
.Build();Built-in Strategies
| Strategy | Behavior | Terminals |
|---|---|---|
AlacrittyReflowStrategy | Bottom-fills the screen after reflow | Alacritty |
WindowsTerminalReflowStrategy | Bottom-fills the screen after reflow | Windows Terminal |
KittyReflowStrategy | Anchors the cursor to its current visual row | Kitty |
WezTermReflowStrategy | Anchors the cursor to its current visual row | WezTerm |
VteReflowStrategy | Cursor-anchored + reflows DECSC saved cursor | GNOME Terminal, Tilix, xfce4-terminal |
GhosttyReflowStrategy | Cursor-anchored + reflows DECSC saved cursor | Ghostty |
FootReflowStrategy | Cursor-anchored + reflows DECSC saved cursor | Foot |
XtermReflowStrategy | No reflow — standard crop/extend | xterm |
ITerm2ReflowStrategy | No reflow — standard crop/extend | iTerm2 |
NoReflowStrategy | No reflow — standard crop/extend (default) | Unknown terminals |
The key difference between strategies is how they handle cursor position during reflow. When narrowing the terminal, soft-wrapped lines split into more rows. AlacrittyReflowStrategy pushes content upward and keeps the bottom of the buffer visible, while KittyReflowStrategy and VteReflowStrategy keep the cursor at the same visual row.
VTE, Ghostty, and Foot additionally reflow the saved cursor position (set via DECSC / ESC 7). This ensures that applications using save/restore cursor across redraws continue to work correctly after a terminal resize.
Each terminal emulator has its own strategy class so that behavior can evolve independently as terminals are updated.
Why separate classes?
Even when two terminals currently behave identically (e.g., Foot and VTE), each gets its own strategy class. This allows us to update one terminal's behavior without affecting others — for example, when Kitty fixes its saved cursor reflow bug, we can update KittyReflowStrategy without changing WezTermReflowStrategy.
See Presentation Adapters for details on configuring reflow per adapter.
Choosing the correct strategy matters
The purpose of terminal reflow is to keep Hex1bTerminal's internal buffer state in sync with what the upstream terminal emulator is actually displaying. If you use ConsolePresentationAdapter with the wrong reflow strategy, the internal state will diverge from the real terminal — leading to cursor misplacement, garbled output, or visual artifacts. When using ConsolePresentationAdapter, prefer WithReflow() (no arguments) to auto-detect the correct strategy for the running terminal. Only override manually if you know the detection is wrong for your environment.
Best-effort strategies
Detailed information about terminal reflow behavior is hard to come by — terminal emulators rarely document their resize logic formally, and behavior can change between versions. These strategies are best effort, based on upstream source code, test suites, and observed behavior.
If you encounter a mismatch between Hex1bTerminal's internal state and what your terminal emulator displays — especially after a recent emulator update — please file an issue with evidence. A screen recording or video showing the before/after state is often the most helpful artifact.
For the HeadlessPresentationAdapter, reflow strategies enable testing of terminal-specific resize behavior without a real terminal. This is useful for validating that your application handles resize correctly across different emulators.
Related Topics
- Using the Emulator — Step-by-step tutorial
- Presentation Adapters — Custom display handling
- Workload Adapters — Custom workload types
- MCP Server — Expose terminals to AI agents
- Testing — Automate terminal interactions