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
PlatformNotSupportedExceptionon 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
Using the Builder — Quick Open (Recommended)
// 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
- Constructor — PTY backend is opened, child process is started, read thread begins.
- PaintDOM — Terminal is resized to match the layout bounds on the first paint and on every subsequent resize.
- Child process exits — Read thread detects EOF, disposes the PTY backend, then closes the containing window automatically.
- 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
PtyNative.Open()creates a PTY master/slave fd pair.- The host re-launches itself (
Environment.ProcessPath) with--pty-shim <slaveFd> <exe> [args]. PtyShim.RunIfShimdetects these arguments, callssetsid/ioctl(TIOCSCTTY), redirects stdin/stdout/stderr to the slave fd, andexecvps the target — replacing the shim process entirely.- The original process reads from the master fd in a background thread, feeds bytes to the
VT100Machine, and callsInvalidateafter each read. - 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+)
- Two anonymous pipes are created: one for keyboard input, one for terminal output.
CreatePseudoConsole(kernel32) is called with those pipe handles, creating a Windows pseudoconsole.CreateProcessis called withEXTENDED_STARTUPINFO_PRESENTand thePROC_THREAD_ATTRIBUTE_PSEUDOCONSOLEattribute, connecting the child's console I/O to the ConPTY.- The original process reads from the output pipe in a background thread, feeds bytes to the
VT100Machine, and callsInvalidate. - 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; useBuild()only when you need custom window configuration. - Do not set
HasFocus = falsewhile the terminal is the only control — keyboard input will stop being forwarded.
See Also
- TerminalBuilder — Fluent API for creating terminals
- WindowBuilder — Manual window configuration
- Controls Static Factory —
Controls.Terminal()