Skip to content

Commit 038b946

Browse files
committed
Update TerminalControl docs for Windows/Linux cross-platform support
1 parent 90bf04b commit 038b946

1 file changed

Lines changed: 51 additions & 29 deletions

File tree

docs/controls/TerminalControl.md

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22

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

5-
> **Platform:** Linux only. The control throws `PlatformNotSupportedException` on other operating systems.
5+
> **Platform:** Linux and Windows 10 1809+ (build 17763). The control throws `PlatformNotSupportedException` on other operating systems.
66
77
## Overview
88

99
`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.
1010

11-
## Critical Setup Requirement
11+
On **Linux** the control uses `openpty` and an in-process shim. On **Windows** it uses the ConPTY API (`CreatePseudoConsole`) introduced in Windows 10 1809.
1212

13-
`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:
13+
## Critical Setup Requirement (Linux only)
14+
15+
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:
1416

1517
```csharp
1618
// Program.cs
1719
if (SharpConsoleUI.PtyShim.RunIfShim(args)) return 127;
1820
```
1921

20-
Without this line the PTY will open but the child process will never start, leaving the terminal blank.
22+
Without this line on Linux the PTY will open but the child process will never start, leaving the terminal blank.
23+
24+
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.
2125

2226
## Properties
2327

@@ -37,14 +41,16 @@ Without this line the PTY will open but the child process will never start, leav
3741
### Using the Builder — Quick Open (Recommended)
3842

3943
```csharp
40-
// Open a bash terminal in a new auto-sized window
44+
// Open the default shell in a new auto-sized window
45+
// (bash on Linux, cmd.exe on Windows)
4146
Controls.Terminal().Open(ws);
4247

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

46-
// Open a different program
47-
Controls.Terminal("/usr/bin/htop").Open(ws);
51+
// Open a specific program
52+
Controls.Terminal("/usr/bin/htop").Open(ws); // Linux
53+
Controls.Terminal("pwsh").Open(ws); // Windows – PowerShell
4854
4955
// Pass arguments
5056
Controls.Terminal("/usr/bin/vim")
@@ -59,7 +65,7 @@ Controls.Terminal("/usr/bin/vim")
5965
Use `Build()` when you need full control over the window configuration:
6066

6167
```csharp
62-
var terminal = Controls.Terminal("/bin/bash").Build();
68+
var terminal = Controls.Terminal().Build();
6369

6470
var window = new WindowBuilder(ws)
6571
.WithTitle(terminal.Title)
@@ -77,15 +83,15 @@ ws.AddWindow(window);
7783

7884
```csharp
7985
var terminal = new TerminalBuilder()
80-
.WithExe("/bin/bash")
86+
.WithExe("/bin/bash") // Linux
8187
.Build();
8288
```
8389

8490
## TerminalBuilder Methods
8591

8692
| Method | Description |
8793
|--------|-------------|
88-
| `WithExe(string exe)` | Set the executable to launch (default: `/bin/bash`) |
94+
| `WithExe(string exe)` | Set the executable to launch (default: `bash` on Linux, `cmd.exe` on Windows) |
8995
| `WithArgs(params string[] args)` | Pass arguments to the executable |
9096
| `Build()` | Create the `TerminalControl` (PTY is opened immediately) |
9197
| `Open(ConsoleWindowSystem ws, int? width, int? height)` | Build and open in a new centered window |
@@ -131,24 +137,52 @@ Both X10 and SGR (1006) mouse encoding protocols are supported, selected by what
131137

132138
## Lifecycle
133139

134-
1. **Constructor** — PTY is opened, shim process is started, read thread begins.
140+
1. **Constructor** — PTY backend is opened, child process is started, read thread begins.
135141
2. **PaintDOM** — Terminal is resized to match the layout bounds on the first paint and on every subsequent resize.
136-
3. **Child process exits** — Read thread detects EOF, closes the master fd, waits for the shim to exit, then closes the containing window automatically.
137-
4. **Dispose** — Closes the master fd, which signals the child process to terminate.
142+
3. **Child process exits** — Read thread detects EOF, disposes the PTY backend, then closes the containing window automatically.
143+
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).
144+
145+
## How It Works
146+
147+
### Linux — openpty + in-process shim
148+
149+
1. `PtyNative.Open()` creates a PTY master/slave fd pair.
150+
2. The host re-launches **itself** (`Environment.ProcessPath`) with `--pty-shim <slaveFd> <exe> [args]`.
151+
3. `PtyShim.RunIfShim` detects these arguments, calls `setsid`/`ioctl(TIOCSCTTY)`, redirects stdin/stdout/stderr to the slave fd, and `execvp`s the target — replacing the shim process entirely.
152+
4. The original process reads from the master fd in a background thread, feeds bytes to the `VT100Machine`, and calls `Invalidate` after each read.
153+
5. Keyboard/mouse input is written back to the master fd as escape sequences.
154+
155+
This design requires no external binaries and works with any process that supports a TTY.
156+
157+
### Windows — ConPTY (Windows 10 1809+)
158+
159+
1. Two anonymous pipes are created: one for keyboard input, one for terminal output.
160+
2. `CreatePseudoConsole` (kernel32) is called with those pipe handles, creating a Windows pseudoconsole.
161+
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.
162+
4. The original process reads from the output pipe in a background thread, feeds bytes to the `VT100Machine`, and calls `Invalidate`.
163+
5. Keyboard/mouse input is written to the input pipe as escape sequences.
164+
165+
No shim or self-relaunch is needed on Windows.
138166

139167
## Examples
140168

141-
### Default Bash Terminal
169+
### Default Shell (cross-platform)
142170

143171
```csharp
144-
// Program.cs — REQUIRED before any UI code
172+
// Program.cs — required on Linux; safe no-op on Windows
145173
if (SharpConsoleUI.PtyShim.RunIfShim(args)) return 127;
146174

147175
// ...
148176
149177
Controls.Terminal().Open(ws);
150178
```
151179

180+
### Windows-specific: PowerShell
181+
182+
```csharp
183+
Controls.Terminal("pwsh").Open(ws);
184+
```
185+
152186
### Specific Program
153187

154188
```csharp
@@ -172,7 +206,7 @@ Controls.Terminal("/usr/bin/vim")
172206
### Terminal Alongside Other Controls
173207

174208
```csharp
175-
var terminal = Controls.Terminal("/bin/bash").Build();
209+
var terminal = Controls.Terminal().Build();
176210

177211
var window = new WindowBuilder(ws)
178212
.WithTitle("Debug Console")
@@ -199,21 +233,9 @@ ws.GlobalKeyPressed += (sender, e) =>
199233
};
200234
```
201235

202-
## How It Works
203-
204-
`TerminalControl` uses a **in-process PTY shim** pattern to avoid the need for a separate helper binary:
205-
206-
1. The host process calls `PtyNative.Open()` to create a PTY master/slave pair.
207-
2. It re-launches **itself** (`Environment.ProcessPath`) with `--pty-shim <slaveFd> <exe> [args]` as arguments.
208-
3. `PtyShim.RunIfShim` detects these arguments, calls `setsid`/`ioctl(TIOCSCTTY)`, redirects stdin/stdout/stderr to the slave fd, and `exec`s the target executable — replacing the shim process entirely.
209-
4. The original process reads from the master fd in a background thread, feeds bytes to the `VT100Machine`, and calls `Invalidate` after each read so the UI repaints.
210-
5. Keyboard/mouse input is written back to the master fd as escape sequences.
211-
212-
This design requires no external binaries and works with any process that supports a TTY.
213-
214236
## Best Practices
215237

216-
- **Always add `PtyShim.RunIfShim(args)` first** — it must run before any console or UI initialisation.
238+
- **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.
217239
- **Let the window close itself** — when the child exits, the window closes automatically; do not force-close it from outside.
218240
- **Prefer `Open()`** for simple cases; use `Build()` only when you need custom window configuration.
219241
- **Do not set `HasFocus = false`** while the terminal is the only control — keyboard input will stop being forwarded.

0 commit comments

Comments
 (0)