TerminalControl

PTY-backed terminal emulator control that embeds a fully interactive shell or process inside any window.

Platform: Linux and Windows 10 1809+ (build 17763). The control throws PlatformNotSupportedException on other operating systems.

Overview

TerminalControl opens a real pseudo-terminal (PTY), spawns a child process inside it, and renders the VT100/xterm-256color screen directly into the SharpConsoleUI buffer. Keyboard and mouse input are forwarded to the process. The terminal resizes automatically when the window is resized.

On Linux the control uses openpty and an in-process shim. On Windows it uses the ConPTY API (CreatePseudoConsole) introduced in Windows 10 1809.

Critical Setup Requirement (Linux only)

On Linux, TerminalControl relies on an in-process PTY shim — the host executable re-launches itself as the slave-side process. You must add the following as the very first line of your Main method, before any UI initialisation:

// Program.cs
if (SharpConsoleUI.PtyShim.RunIfShim(args)) return 127;

Without this line on Linux the PTY will open but the child process will never start, leaving the terminal blank.

On Windows no shim is required. PtyShim.RunIfShim is a no-op on Windows (returns false immediately), so the call is safe to keep in cross-platform code.

Properties

Property Type Default Description
Title string " Terminal — <exe>" Window title derived from the launched executable
HasFocus bool true Whether the control has keyboard focus
IsEnabled bool true Enable/disable keyboard and mouse input forwarding
Visible bool true Show/hide the control
Container IContainer? null Set by the window when the control is added
Margin Margin 0,0,0,0 Layout margin around the control
HorizontalAlignment HorizontalAlignment Stretch Fills available width
VerticalAlignment VerticalAlignment Fill Fills available height

Creating a Terminal

// Open the default shell in a new auto-sized window
// (bash on Linux, cmd.exe on Windows)
Controls.Terminal().Open(ws);

// Specify an explicit size (cols × rows)
Controls.Terminal().Open(ws, width: 120, height: 40);

// Open a specific program
Controls.Terminal("/usr/bin/htop").Open(ws);          // Linux
Controls.Terminal("pwsh").Open(ws);                   // Windows – PowerShell

// Pass arguments
Controls.Terminal("/usr/bin/vim")
    .WithArgs("/etc/hosts")
    .Open(ws, width: 100, height: 35);

Open creates the TerminalControl, wraps it in a centered closable window, and registers it with the window system. When the child process exits the window closes automatically.

Using the Builder — Manual Window Wiring

Use Build() when you need full control over the window configuration:

var terminal = Controls.Terminal().Build();

var window = new WindowBuilder(ws)
    .WithTitle(terminal.Title)
    .WithSize(82, 26)
    .Centered()
    .Closable(true)
    .Resizable(true)
    .AddControl(terminal)
    .Build();

ws.AddWindow(window);

Using TerminalBuilder Directly

var terminal = new TerminalBuilder()
    .WithExe("/bin/bash")   // Linux
    .Build();

TerminalBuilder Methods

Method Description
WithExe(string exe) Set the executable to launch (default: bash on Linux, cmd.exe on Windows)
WithArgs(params string[] args) Pass arguments to the executable
Build() Create the TerminalControl (PTY is opened immediately)
Open(ConsoleWindowSystem ws, int? width, int? height) Build and open in a new centered window

Open Method Parameters

Parameter Type Default Description
ws ConsoleWindowSystem required The window system to open the terminal in
width int? null Terminal columns. When null: desktop width − 6, minimum 60
height int? null Terminal rows. When null: desktop height − 6, minimum 20

Keyboard Support

All standard xterm-256color key sequences are encoded and forwarded to the PTY.

Key Sequence sent
Printable chars UTF-8 bytes
Enter CR (0x0D)
Backspace DEL (0x7F)
Tab HT (0x09)
Escape ESC (0x1B)
Arrow keys ESC[A/B/C/D or ESCO A/B/C/D (application cursor mode)
Home / End ESCOH / ESCOF
Page Up / Down ESC[5~ / ESC[6~
Delete / Insert ESC[3~ / ESC[2~
F1–F12 Standard xterm sequences
Ctrl+C/D/Z/L/A/E/U/W/K/R Corresponding control bytes

Mouse Support

Mouse events are forwarded when the running application enables mouse reporting (e.g. vim, htop, ncurses apps).

Event Description
Button press/release Button 1–3 press and release
Scroll wheel Wheel up/down forwarded as buttons 64/65
Drag Button drag in button-event (1002) and any-event (1003) modes
Mouse move Position reporting in any-event mode (1003)

Both X10 and SGR (1006) mouse encoding protocols are supported, selected by what the child process requests.

Lifecycle

  1. Constructor — PTY backend is opened, child process is started, read thread begins.
  2. PaintDOM — Terminal is resized to match the layout bounds on the first paint and on every subsequent resize.
  3. Child process exits — Read thread detects EOF, disposes the PTY backend, then closes the containing window automatically.
  4. Dispose — Disposes the PTY backend, which signals the child process to terminate (closes the master fd on Linux; closes the ConPTY and pipes on Windows).

How It Works

Linux — openpty + in-process shim

  1. PtyNative.Open() creates a PTY master/slave fd pair.
  2. The host re-launches itself (Environment.ProcessPath) with --pty-shim <slaveFd> <exe> [args].
  3. PtyShim.RunIfShim detects these arguments, calls setsid/ioctl(TIOCSCTTY), redirects stdin/stdout/stderr to the slave fd, and execvps the target — replacing the shim process entirely.
  4. The original process reads from the master fd in a background thread, feeds bytes to the VT100Machine, and calls Invalidate after each read.
  5. Keyboard/mouse input is written back to the master fd as escape sequences.

This design requires no external binaries and works with any process that supports a TTY.

Windows — ConPTY (Windows 10 1809+)

  1. Two anonymous pipes are created: one for keyboard input, one for terminal output.
  2. CreatePseudoConsole (kernel32) is called with those pipe handles, creating a Windows pseudoconsole.
  3. CreateProcess is called with EXTENDED_STARTUPINFO_PRESENT and the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute, connecting the child's console I/O to the ConPTY.
  4. The original process reads from the output pipe in a background thread, feeds bytes to the VT100Machine, and calls Invalidate.
  5. Keyboard/mouse input is written to the input pipe as escape sequences.

No shim or self-relaunch is needed on Windows.

Examples

Default Shell (cross-platform)

// Program.cs — required on Linux; safe no-op on Windows
if (SharpConsoleUI.PtyShim.RunIfShim(args)) return 127;

// ...

Controls.Terminal().Open(ws);

Windows-specific: PowerShell

Controls.Terminal("pwsh").Open(ws);

Specific Program

Controls.Terminal("/usr/bin/htop").Open(ws);

Custom Size

Controls.Terminal().Open(ws, width: 132, height: 43);

Vim Editing a File

Controls.Terminal("/usr/bin/vim")
    .WithArgs("/etc/hosts")
    .Open(ws, width: 100, height: 40);

Terminal Alongside Other Controls

var terminal = Controls.Terminal().Build();

var window = new WindowBuilder(ws)
    .WithTitle("Debug Console")
    .WithSize(100, 35)
    .AtPosition(2, 2)
    .Resizable(true)
    .Closable(true)
    .AddControl(terminal)
    .Build();

ws.AddWindow(window);

Keyboard Shortcut to Open Terminal

ws.GlobalKeyPressed += (sender, e) =>
{
    if (e.KeyInfo.Key == ConsoleKey.F7)
    {
        Controls.Terminal().Open(ws);
        e.Handled = true;
    }
};

Best Practices

  • Linux: always add PtyShim.RunIfShim(args) first — it must run before any console or UI initialisation. The call is a safe no-op on Windows.
  • Let the window close itself — when the child exits, the window closes automatically; do not force-close it from outside.
  • Prefer Open() for simple cases; use Build() only when you need custom window configuration.
  • Do not set HasFocus = false while the terminal is the only control — keyboard input will stop being forwarded.

See Also


Back to Controls | Back to Main Documentation