Cross-platform async PTY with integrated terminal emulation for Python.
stitch-pty is a high-performance, cross-platform pseudo-terminal (PTY) library for Python, written in Rust with PyO3 bindings. It provides true PTY semantics on Linux, macOS, and Windows, with a built-in VT100/VT220/xterm-compatible terminal emulator for rendering ANSI escape sequences with scrollback history.
| Feature | stitch-pty | pty stdlib |
pexpect |
|---|---|---|---|
| Cross-platform | ✅ Linux / macOS / Windows | ❌ POSIX only | ❌ POSIX only |
| Async I/O | ✅ Native asyncio |
❌ Blocking | ❌ Blocking |
| Zero GIL contention | ✅ PyO3 native async | ✅ | ✅ |
| Terminal emulation | ✅ Built-in (scrollback + styled viewport) | ❌ | ❌ |
| No zombie processes | ✅ Background reaping (50ms polling) | ||
| Type-safe Python API | ✅ Full mypy support | ✅ | ✅ |
- Why stitch-pty?
- Table of Contents
- Quick Start
- Installation
- API Reference
- Architecture
- Examples
- Building from Source
- Source Projects & Dependencies
- Platform Support
- License
import asyncio
from stitch_pty import spawn
async def main():
# Spawn a shell in a real PTY
session = await spawn("bash", ["-i"])
# Read output (auto-fed into the terminal emulator)
data = await session.read(4096)
print(session.display) # visible screen → list[str]
print(session.scrollback) # scrollback history → list[str]
print(session.full_display) # history + visible → list[str]
# Write to the PTY
await session.write(b"echo hello from stitch-pty\n")
# Resize the terminal
session.resize(50, 120)
# Graceful shutdown
await session.terminate(5.0)
asyncio.run(main())pip install stitch-ptyPre-built wheels are available for:
| Platform | Architectures |
|---|---|
| Linux | x86_64, aarch64 (manylinux_2_28) |
| macOS | x86_64, arm64 (universal2) |
| Windows | x86_64 |
Requirements: Python ≥ 3.12
Spawn a program in a PTY and return a session handle.
| Parameter | Type | Default | Description |
|---|---|---|---|
program |
str |
— | The executable to run |
args |
list[str] |
[] |
Command-line arguments |
env |
dict[str, str] |
None |
Environment variables (inherits from parent if None) |
winsize |
Winsize | None |
Auto-detected | Initial terminal size (defaults to 24×80) |
Returns: PtySession — a combined PTY I/O + child process manager with integrated terminal emulation.
Example:
session = await spawn("python3", ["-c", "print('hello')"])
output = await session.interact()
print(output.decode()) # b"hello\n"Async. Opens a PTY pair without spawning a child process.
| Parameter | Type | Default | Description |
|---|---|---|---|
winsize |
Winsize | None |
24×80 | Terminal size |
Returns: PtyMaster — raw PTY I/O handle (no terminal emulation, no child management).
Example:
pty = await open_pty(Winsize(24, 80, 0, 0))
data = await pty.read(4096)
await pty.write(b"hello\n")Frozen dataclass returned by await wait() (or None if the child was already reaped).
from stitch_pty import ExitStatus
status = await session.wait()
if status:
print(f"PID {status.pid} exited with code {status.exit_code}")| Field | Type | Description |
|---|---|---|
pid |
int |
PID of the process that exited |
exit_code |
int | None |
Exit code, or None if killed by a signal |
signal |
int | None |
Signal number, or None if exited normally |
core_dumped |
bool |
Whether a core dump was produced |
The primary interface for most use cases. Combines PTY I/O, child process management, and terminal emulation.
| Method | Signature | Description |
|---|---|---|
read |
await read(size=4096) → bytes |
Read from PTY (auto-feeds terminal) |
read_timeout |
await read_timeout(size, timeout) → bytes |
Read with timeout (raises IOError on timeout) |
write |
await write(data) → int |
Write bytes to PTY, returns bytes written |
write_all |
await write_all(data) → None |
Write all bytes (handles partial writes) |
resize |
resize(rows, cols) → None |
Resize terminal (forwards to PTY backend) |
wait |
await wait() → ExitStatus | None |
Wait for child exit; returns ExitStatus or None if already reaped |
terminate |
await terminate(grace_period=5.0) → None |
SIGTERM → wait → SIGKILL fallback |
kill |
kill() → None |
Force kill immediately |
interrupt |
interrupt() → None |
Send Ctrl+C (SIGINT on POSIX, GenerateConsoleCtrlEvent on Windows) |
send_signal |
send_signal(num) → None |
Send arbitrary signal number |
interact |
await interact(input_data=None, timeout=None) → bytes |
Write input, read until EOF (high-level) |
read_all |
await read_all(timeout=1.0) → bytes |
Read all output until timeout |
expect |
await expect(pattern, timeout=30.0) → bytes |
pexpect-style: read until pattern found |
Properties:
| Property | Type | Description |
|---|---|---|
is_alive |
bool |
Process still running? |
terminal |
TerminalState |
Direct access to terminal emulation state |
display |
list[str] |
Visible screen (one string per row) |
scrollback |
list[str] |
Scrollback history |
full_display |
list[str] |
History + visible screen |
raw_output |
bytes |
All raw bytes read (unparsed) |
Context Manager:
async with spawn("bash", ["-i"]) as session:
await session.write(b"echo hello\n")
output = await session.interact()
# session.terminate(2.0) called automatically on exitRaw PTY I/O without terminal emulation or child management.
| Method | Signature | Description |
|---|---|---|
read |
await read(size=4096) → bytes |
Read from PTY master |
read_timeout |
await read_timeout(size, timeout) → bytes |
Read with timeout |
write |
await write(data) → int |
Write bytes, returns count |
write_all |
await write_all(data) → None |
Write all bytes |
set_winsize |
set_winsize(rows, cols, xpixel=0, ypixel=0) → None |
Set window size |
get_winsize |
get_winsize() → Winsize |
Get current size |
fd |
property → int |
Raw file descriptor (Unix) / -1 (Windows) |
is_open |
property → bool |
PTY still open? |
Child process management.
| Property/Method | Signature | Description |
|---|---|---|
pid |
property → int |
Child process PID |
is_running |
property → bool |
Process still running? |
wait |
await wait() → ExitStatus | None |
Wait for exit; returns ExitStatus or None if already reaped |
terminate |
await terminate(grace_period=5.0) → None |
SIGTERM → wait → SIGKILL |
kill |
kill() → None |
Force kill |
interrupt |
interrupt() → None |
Send Ctrl+C |
send_signal |
send_signal(num) → None |
Send signal number |
VT100/VT220/xterm-compatible terminal emulation with scrollback.
| Method | Signature | Description |
|---|---|---|
feed |
feed(data: bytes) → None |
Feed raw bytes (parses ANSI, updates screen) |
display |
display() → list[str] |
Full display (scrollback + visible) |
visible_display |
visible_display() → list[str] |
Visible screen only |
history_display |
history_display() → list[str] |
Scrollback history only |
styled_viewport |
styled_viewport() → list[list[tuple[str, str, str, int]]] |
Full buffer as styled cells: (text, fg, bg, attrs_bitmask) |
total_lines |
total_lines() → int |
Total lines = history + visible |
absolute_cursor |
absolute_cursor() → tuple[int, int] |
(x, history_len + on_screen_y) |
dirty |
dirty() → list[int] |
Modified row indices |
resize |
resize(lines, cols) → None |
Resize screen buffer |
reset |
reset() → None |
Reset terminal + clear history |
| Property | Type | Description |
|---|---|---|
cursor_x |
int |
Cursor column (0-indexed, visible area) |
cursor_y |
int |
Cursor row (0-indexed, visible area) |
title |
str |
Window title (from OSC sequences) |
history_size |
int |
Current scrollback line count |
scrollback_lines |
int |
Scrollback capacity |
set_scrollback_lines |
set_scrollback_lines(n) → None |
Set capacity (trims excess) |
Styled Viewport Cell Layout:
# Each cell: (text, fg, bg, attrs_bitmask)
# fg/bg formats: "default", ANSI name ("red", "brightblue"), or 6-hex RGB ("ff0000")
# attrs bitmask: bit 0=bold, 1=dim, 2=italics, 3=underscore,
# 4=blink, 5=reverse, 6=hidden, 7=strikethroughTerminal window dimensions.
| Property | Type | Description |
|---|---|---|
rows |
u16 |
Number of rows |
cols |
u16 |
Number of columns |
xpixel |
u16 |
Width in pixels |
ypixel |
u16 |
Height in pixels |
ws = Winsize(50, 120, 0, 0)| Exception | Inherits | Raised On |
|---|---|---|
PtyError |
Exception |
PTY open/operation failures, platform errors |
ProcessError |
PtyError |
Spawn/kill failures, process not running |
IOError |
PtyError |
I/O errors, timeouts, winsize failures |
┌─────────────────────────────────────────────────────────────────┐
│ Python Layer │
│ ┌──────────────┐ ┌────────────────────────────────────────┐ │
│ │ PtySession │ │ TerminalState (pyte_rs) │ │
│ │ (Python) │ │ ├─ Parser (ANSI stream → Screen) │ │
│ │ │ │ ├─ HistoryScreen (scrollback buffer) │ │
│ │ │ │ └─ Screen (Char grid, cursor, SGR) │ │
│ └──────┬───────┘ └────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴──────────────────────────────────────────────────┐ │
│ │ stitch_pty._core (PyO3 / Rust) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Platform Abstraction Layer │ │ │
│ │ │ ┌──────────────────┐ ┌──────────────────────┐ │ │ │
│ │ │ │ POSIX Backend │ │ Windows Backend │ │ │ │
│ │ │ │ ── openpty() │ │ ── ConPTY (dyn load)│ │ │ │
│ │ │ │ ── fork() │ │ ── NamedPipe (async)│ │ │ │
│ │ │ │ ── AsyncFd │ │ ── CreateProcessW │ │ │ │
│ │ │ │ ── waitpid() │ │ ── GetExitCodeProc │ │ │ │
│ │ │ └──────────────────┘ └──────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Component | Detail |
|---|---|
| PTY creation | openpty(3) → master/slave pair, O_NONBLOCK on master |
| Process spawn | fork() → child: setsid() + TIOCSCTTY + dup2()×3 + execvpe() |
| Async I/O | tokio::io::AsyncFd over raw FD with try_io pattern |
| Reaping | Background tokio::spawn polls waitpid(WNOHANG) every 50ms |
| Signal delivery | All signals sent to process group (-pgid) via nix::sys::signal |
| Resize | TIOCSWINSZ ioctl + SIGWINCH to process group via tcgetpgrp |
| FD leak fix | close_random_fds() closes FDs > 2 via /dev/fd (critical for macOS Big Sur) |
| Signal reset | Pre-exec: resets SIGCHLD, SIGHUP, SIGINT, SIGTERM, SIGALRM to SIG_DFL |
Async I/O Pattern:
loop {
let mut guard = self.async_fd.readable().await?;
match guard.try_io(|inner| {
libc::read(*inner.get_ref(), buf.as_mut_ptr(), buf.len())
}) {
Ok(result) => return result,
Err(_would_block) => continue,
}
}| Component | Detail |
|---|---|
| ConPTY loading | Dynamic GetProcAddress from kernel32.dll (graceful fallback on older Windows) |
| PTY creation | CreatePseudoConsole(size, stdin_handle, stdout_handle) |
| Pipe plumbing | Two tokio::net::NamedPipeServer instances (input + output) |
| Pipe naming | \\.\pipe\stitch-pty-{pid}-{counter} (unique per instance) |
| Process spawn | CreateProcessW with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE via SafeAttributeList |
| Async I/O | tokio::net::NamedPipeServer → true async via IOCP (no spawn_blocking) |
| Reaping | Background poll of GetExitCodeProcess every 50ms (no WaitForSingleObject) |
| Signal delivery | Ctrl+C → GenerateConsoleCtrlEvent; SIGTERM/KILL → TerminateProcess |
| Resize | ResizePseudoConsole (no signal forwarded to child) |
| Startup handshake | Sends DSR reply \x1b[1;1R to conhost to begin relaying child output |
| Command quoting | ArgvQuote algorithm: proper escaping of spaces, quotes, trailing backslashes |
Send-Safe Windows Types:
SendSyncHandle wraps HANDLE with Send + Sync + Clone, allowing Windows non-Send types to cross .await boundaries. All non-Send locals (STARTUPINFOEXW, PROCESS_INFORMATION, attribute list) are scoped to a block ending before the first .await after CreateProcessW.
Embedded from pyte_rs. Provides VT100/VT220/xterm-compatible rendering.
ECMA-48 state machine with 10 states:
| State | Enters On | Exits On |
|---|---|---|
Ground |
Default | ESC (\x1b), C0 control |
CsiEntry |
ESC [ |
Final byte (@–~) |
CsiParam |
0–9, ;, : |
Final byte |
CsiIntermediate |
–/ |
Final byte |
CsiIgnore |
>–? in param |
Final byte |
OscString |
ESC ] |
BEL, ST, ESC |
DcsEntry |
ESC P |
Hook byte |
DcsPassthrough |
After hook | \x9c, ESC, SUB, CAN |
Escape |
ESC | Final byte / intermediate |
EscapeIntermediate |
ESC + –/ |
Final byte |
Parser Limits: Max 32 params (sub-param groups), 2 intermediates, 16 OSC params, 1024 OSC bytes, 4-byte UTF-8 partial buffer.
| Feature | Detail |
|---|---|
| Char cell | data, fg, bg, bold, dim, italics, underscore, blink, reverse, hidden, strikethrough |
| SGR colors | 8 ANSI, 8 aixterm bright, 256-color, 24-bit RGB (with colon subparams) |
| Modes | Public (IRM, LNM) + Private (DECOM, DECAWM, DECCOLM, DECTCEM, DECSCNM) + Extended (mouse, bracketed paste) |
| Character sets | G0/G1 with DEC Special line drawing |
| Unicode | Width-1/2 chars, combining marks, CJK, emoji |
| Kitty Keyboard Protocol | Mode push/pop/replace |
| Dirty tracking | BTreeSet<usize> of modified row indices |
| Method | Description |
|---|---|
scroll_up_with_history(rows) |
Push scrolled lines to history |
scroll_down_with_history(rows) |
Pop lines from history |
total_lines() |
history_len + visible_lines |
absolute_cursor() |
(x, history_len + on_screen_y) |
styled_viewport() |
Full buffer as (text, fg, bg, attrs_bitmask) cells |
PyO3 native async automatically releases the GIL at .await:
Python call (GIL held)
→ future_into_py (GIL released)
→ tokio I/O await (GIL released)
→ completion: GIL reacquired
→ Rust value → Python object conversion
GIL is acquired only for:
- Converting
Vec<u8>→PyBytes - Raising
PyErrexceptions
POSIX ownership:
PtyPair (owns master_fd + slave_fd)
├── master_fd → UnixPtyMaster (via mem::forget)
│ └── Drop: close(fd)
└── slave_fd → child process (via fork)
└── Child closes on exec, parent closes after fork
Windows ownership:
WinPtyBackend (owns HPCON + NamedPipeServer handles)
├── input_pipe → NamedPipeServer (Drop: drop)
├── output_pipe → NamedPipeServer (Drop: drop)
└── conpty → ConPTY (Drop: ClosePseudoConsole)
RAII guarantees:
| Type | Drop Behavior |
|---|---|
UnixPtyMaster |
close(raw_fd) |
WinPtyBackend |
ClosePseudoConsole + drop named pipes |
UnixChildProcess |
kill(SIGKILL) if running |
WinChildProcess |
TerminateProcess if running; CloseHandle |
import asyncio
from stitch_pty import spawn
async def main():
session = await spawn("echo", ["hello", "world"])
output = await session.interact()
print(output.decode()) # b"hello world\n"
asyncio.run(main())import asyncio
from stitch_pty import spawn
async def main():
session = await spawn("bash", ["-i"])
# Send a command
await session.write(b"ls -la\n")
# Wait for output
output = await session.interact()
print(output.decode())
# Cleanup
await session.terminate()
asyncio.run(main())import asyncio
from stitch_pty import spawn
async def main():
session = await spawn("bash", ["-i"])
# Wait for a prompt
prompt = await session.expect(b"$ ", timeout=10.0)
print(f"Got: {prompt.decode()}")
# Send command
await session.write(b"uname -a\n")
# Wait for output
output = await session.expect(b"\n", timeout=5.0)
print(f"Output: {output.decode()}")
await session.terminate()
asyncio.run(main())import asyncio
from stitch_pty import open_pty, Winsize
async def main():
pty = await open_pty(Winsize(24, 80, 0, 0))
# Write and read raw bytes
await pty.write(b"hello from raw PTY\n")
data = await pty.read(4096)
print(data.decode())
asyncio.run(main())A full terminal emulator with real-time rendering, keyboard input, resize handling, and styled viewport:
pip install PySide6 stitch-pty
python examples/terminal_emulator.py
python examples/terminal_emulator.py --cmd "whoami"
python examples/terminal_emulator.py --rows 30 --cols 100Key features demonstrated:
- Async PTY + Qt event loop integration (background thread)
- Real-time styled rendering via
styled_viewport()with HTML - Cursor position tracking via
absolute_cursor() - Keyboard input forwarding with Ctrl+key → ANSI sequences
- Window resize forwarding (rate-limited during drag)
- Graceful shutdown on window close
stitch-pty is designed to be used both as a standalone Rust crate and as a Python extension. The Python bindings are decoupled and gated behind the python feature flag.
| Requirement | Version |
|---|---|
| Rust | ≥ 1.85 |
| Python | ≥ 3.12 |
| maturin | ≥ 1.8 |
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install maturin
pip install maturinTo use stitch-pty in a Rust project, add it to your Cargo.toml. The Python bindings are disabled by default, allowing for a pure Rust build without PyO3 overhead.
[dependencies]
stitch-pty = "0.5.5"To build and test the pure Rust code locally:
cargo build
cargo testThe Python extension is built using maturin. The pyproject.toml is configured to automatically enable the python feature when building wheels.
# Clone
git clone https://github.com/stitch-pty/stitch-pty.git
cd stitch-pty
# Development build (fast, unoptimized)
maturin develop
# Alternatively, using uv to test:
uv run --extra dev pytest
# Run Python tests (platform-specific tests auto-skip)
pytest tests/ -v
# Type check
mypy python/stitch_pty/
# Lint
ruff check .
# Production wheel
maturin build --release# Linux ARM64 from x86_64 host
maturin build --release --target aarch64-unknown-linux-gnu
# macOS universal2
maturin build --release --target universal2-apple-darwin
# Windows from Linux (requires cross toolchain)
maturin build --release --target x86_64-pc-windows-gnustitch-pty is built on three foundational open-source projects. Each contributed significant portions of code, architecture patterns, and design decisions that were adapted, extended, or embedded into this project.
| Property | Detail |
|---|---|
| Source | ../other packages/portable-pty-main |
| Repository | github.com/wezterm/portable-pty |
| Version | 0.9.0 |
| Author | Wez Furlong |
| License | MIT |
| Role | Cross-platform PTY backend patterns (traits, ChildKiller, ExitStatus, PtySize) |
What was taken:
ChildKillertrait — deadlock-free kill/wait separation pattern (cloneable killer split fromChildBackendto avoid blocking.waitthreads)ExitStatus/ProcessExitstructs — unified exit code + signal representationPtySize→Winsize— window size struct with rows/cols/pixelsclose_random_fds()— macOS Big Sur / Linux FD leak prevention (closes all FDs > 2 via/dev/fdlisting)ArgvQuotealgorithm — proper Windows command-line quoting (backslash/quote escaping)SafeAttributeListpattern — Win32PROC_THREAD_ATTRIBUTE_LISTlifecycle management (Initialize → Update → Delete) for ConPTY attachmentProcessSignaller— cross-platform signal delivery abstraction
What changed:
- Rewritten for tokio async (portable-pty uses blocking I/O)
- Replaced
nix 0.28→nix 0.31with additional features (signal, poll, ioctl) - Replaced
winapi→windowscrate (0.62, modern MSFT bindings) - Replaced
anyhow→thiserrorfor zero-cost error propagation - Added Tokio NamedPipes for Windows async I/O (portable-pty uses
spawn_blocking) - Added dynamic ConPTY loading via
GetProcAddressfor graceful fallback - GIL management for Python integration
| Property | Detail |
|---|---|
| Source | ../other packages/pyte-master |
| Repository | github.com/selectel/pyte |
| Version | 0.8.3 (dev) |
| License | LGPL-3.0 |
| Role | VT100/VT220/xterm-compatible terminal screen emulation (embedded as terminal/) |
What was taken:
Screen— 2D Char grid, cursor management, tab stops, dirty trackingCharcell — data, fg/bg colors, bold/dim/italics/underscore/blink/reverse/hidden/strikethroughCursor— position, attributes, save/restore stackMargins— scroll region top/bottomCursorStyle— block/underline/beam variants- SGR color handling — 8 ANSI, 8 aixterm bright, 256-color palette, 24-bit RGB
- Mode management — public ANSI (IRM, LNM) + private DEC (DECOM, DECAWM, DECCOLM, etc.)
- Character set designation — G0/G1 with DEC Special line drawing map
- OSC title/icon dispatch — OSC 0/1/2; icon name and window title
- CSI cursor movement — CUU/CUD/CUF/CUB/CUP/CNL/CPL/CHA
- CSI erase — ED (display), EL (line), DC (characters), DL/IL (lines)
- Unicode handling —
unicode-widthfor width-1/width-2 chars, combining marks - Kitty Keyboard Protocol — mode push/pop/replace
- Device status — DA0/DA1 identification, cursor position report
What changed:
- Ported from Python to Rust with PyO3 bindings
- Added
HistoryScreen— scrollback buffer (not in upstream pyte) - Added
styled_viewport()— full buffer as(text, fg, bg, attrs_bitmask)cells - Added
total_lines()andabsolute_cursor()for scroll-aware positioning - Added
unicode-segmentationfor proper grapheme boundary handling - SGR subparameter parsing (
:as param separator) — differs from vte crate - Color stored as hex strings (
"ff0000") instead of RGB tuples for Python serialization
| Property | Detail |
|---|---|
| Source | ../other packages/vte-master |
| Repository | github.com/alacritty/vte |
| Version | 0.15.0 |
| Authors | Joe Wilm, Christian Duerr |
| License | Apache-2.0 OR MIT |
| Role | ECMA-48 ANSI escape sequence state machine (embedded as terminal/ansi_parser.rs) |
What was taken:
- ECMA-48 state machine — 10-state ANSI parser (Ground, CsiEntry, CsiParam, CsiIntermediate, CsiIgnore, OscString, DcsEntry, DcsPassthrough, Escape, EscapeIntermediate)
Parserstruct — state, intermediates, params, UTF-8 partial buffer, OSC raw bufferParamsstruct — sub-parameter groups withMAX_PARAMS = 32limitPerformtrait —print,execute,hook,put,unhook,osc_dispatch,csi_dispatch,esc_dispatch,terminated- UTF-8 handling — partial byte buffer (4 bytes), invalid byte replacement with
\u{FFFD} - OSC handling —
MAX_OSC_PARAMS = 16,MAX_OSC_RAW = 1024, BEL/ST termination - DCS passthrough — hook/passthrough/unhook state transitions
- C0/C1 control handling — C0 (
\x00–\x1f), C1 (\x80–\x9f) - Parameter parsing —
:subparameter separator,;param separator, overflow saturation
What changed:
- Embedded directly as source (not a crate dependency) — allows custom
Performimpl Performtrait extended withesc_dispatchfor legacy ESC sequences- CSI parameter parsing:
:treated as param separator (not subparam like upstream vte) - Added
escape.rsdesignator constants (RIS, IND, NEL, HTS, etc.) - Added
charsets.rsDEC Special line drawing map - Added
control.rsC0/C1 constants - Added extensive unit tests for parser edge cases (overflow, reset, partial UTF-8)
| Source Project | License | How Used | Lines Contributed |
|---|---|---|---|
| portable-pty | MIT | PTY backend patterns, ChildKiller, ExitStatus, ArgvQuote, SafeAttributeList |
~200 lines (patterns) |
| pyte | LGPL-3.0 | Terminal emulation (Screen, Char, Cursor, SGR, modes, character sets) |
~1,200 lines (embedded) |
| vte | Apache-2.0 OR MIT | ANSI parser state machine (Parser, Params, Perform) |
~600 lines (embedded) |
Note: stitch-pty is licensed MIT OR Apache-2.0. The LGPL-3.0 dependency (pyte) is satisfied by the "exception to section 3" clause: the terminal emulation is embedded as a module, not linked as a library. The MIT and Apache-2.0 licenses are compatible with stitch-pty's dual licensing.
| Platform | Backend | Signal Support | Resize Signal | Exit Info | Status |
|---|---|---|---|---|---|
| Linux | POSIX openpty() + fork() |
Full | ✅ SIGWINCH |
code + signal + core_dumped | ✅ Complete |
| macOS | POSIX openpty() + fork() |
Full | ✅ SIGWINCH |
code + signal + core_dumped | ✅ Complete |
| Windows 10 1809+ | ConPTY + NamedPipes | Ctrl+C only | ❌ No signal | code only | ✅ Complete |
| Windows <10 | CreateProcess + pipes |
❌ Limited | ❌ | code only |
| Feature | POSIX | Windows |
|---|---|---|
| PTY backend | openpty(3) + fork() + execvpe() |
ConPTY (CreatePseudoConsole) + CreateProcessW |
| I/O model | tokio::AsyncFd over raw FDs |
tokio::NamedPipeServer (IOCP) |
| Signal delivery | Full via nix::sys::signal (SIGINT, SIGTERM, SIGKILL, SIGWINCH) |
Ctrl+C (GenerateConsoleCtrlEvent), SIGTERM/KILL → TerminateProcess |
| Resize signal | SIGWINCH forwarded to process group via tcgetpgrp |
No signal; ResizePseudoConsole only |
| Exit info | exit_code + signal + core_dumped |
exit_code only |
| Pipe plumbing | Single FD pair (master/slave) | Two named pipes (input/output) + connect() to arm IOCP |
| Startup handshake | N/A | DSR reply (\x1b[1;1R) to conhost before child output flows |
| FD leak fix | close_random_fds() (macOS/Linux) |
N/A |
MIT OR Apache-2.0