stitch-pty
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.
Why stitch-pty?
| Feature | stitch-pty | pty stdlib |
pexpect |
|---|---|---|---|
| Cross-platform | ✅ Linux / macOS / Windows | ❌ POSIX only | ❌ POSIX only |
| Async I/O | ✅ Native asyncio |
❌ Blocking | ❌ Blocking |
| GIL released during I/O | ✅ Tokio .await |
✅ blocking read | ✅ blocking read |
| Terminal emulation | ✅ Built-in (scrollback + styled viewport) | ❌ | ❌ |
| No zombie processes | ✅ Background reaping (50ms polling) | ⚠️ Manual | ⚠️ Manual |
| Type-safe Python API | ✅ Full mypy support | ✅ | ✅ |
Table of Contents
- Why stitch-pty?
- Table of Contents
- Quick Start
- Installation
- API Reference
- Architecture
- Examples
- Building from Source
- Source Projects & Dependencies
- Platform Support
- License
Quick Start
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())
Installation
pip install stitch-pty
Pre-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
API Reference
Core Functions
spawn(program, args=None, env=None, winsize=None, scrollback=1000, raw_output_cap=1_048_576, cwd=None) → PtySession
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) |
scrollback |
int |
1000 |
Terminal scrollback capacity in lines |
raw_output_cap |
int | None |
1_048_576 |
Max bytes retained in session.raw_output (None = unbounded) |
cwd |
str | None |
None |
Child working directory (inherits parent's if None; missing path raises PtyError) |
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"
poll_events() → list[tuple[str, object]] (PtySession method)
Drain the ordered low-frequency event log for this frame — one FFI crossing
instead of polling each signal separately. Tags: ("bell", None) (also
consumes take_bell()), ("title", str), ("icon", str), ("cwd", str),
("altscreen", bool), ("scrollback_grew", int).
for tag, payload in session.poll_events():
if tag == "bell":
flash_tab_dot()
elif tag == "title":
set_tab_title(payload)
open_pty(winsize=None) → PtyMaster
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")
ExitStatus
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 |
PtySession
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); returns b"" on EOF (child exited) |
read_timeout |
await read_timeout(size, timeout) → bytes |
Read with timeout (raises PtyError on timeout; b"" on EOF) |
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 PTY backend and emulator (reflows on column change) |
wait |
await wait(timeout=None) → ExitStatus | None |
Wait for child exit; returns ExitStatus or None if already reaped; raises TimeoutError past timeout |
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(patterns, timeout=30.0) → ExpectResult |
pexpect-style: bytes/str/regex, single or list; .buffer on timeouts |
poll_events |
poll_events() → list[tuple[str, object]] |
Drain ordered events (bell, title, icon, cwd, altscreen, scrollback_grew) |
take_bell |
take_bell() → bool |
Edge-triggered BEL check (resets the flag) |
take_dirty_rows |
take_dirty_rows() → list[int] |
Drain dirty row indices (sorted, empty afterwards) |
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 |
cwd |
str | None |
Shell cwd from OSC 7 / OSC 9;9, None until reported |
visible_lines |
int |
Visible height in rows, O(1) (prefer over len(display)) |
visible_columns |
int |
Visible width in columns, O(1) |
raw_output |
bytes |
Last raw_output_cap raw bytes read (unparsed; sliding window, default 1 MiB) |
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 exit
PtyMaster
Raw 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? |
PtyChild
Child process management.
| Property/Method | Signature | Description |
|---|---|---|
pid |
property → int |
Child process PID |
is_running |
property → bool |
Process still running? |
wait |
await wait(timeout=None) → ExitStatus | None |
Wait for exit; returns ExitStatus or None if already reaped; raises TimeoutError past timeout |
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 |
TerminalState
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 |
take_dirty_rows |
take_dirty_rows() → list[int] |
Drain dirty rows (sorted, empty afterwards) |
take_bell |
take_bell() → bool |
Edge-triggered BEL check (resets the flag) |
poll_events |
poll_events() → list[tuple[str, object]] |
Drain ordered events (log capped at 1024, drop-oldest) |
resize |
resize(lines, cols) → None |
Resize screen buffer (column change reflows; zero clamps to 1) |
reset |
reset() → None |
Reset terminal + clear history |
set_scrollback_lines |
set_scrollback_lines(n) → None |
Set capacity (trims excess) |
| 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) |
cwd |
str | None |
Shell cwd from OSC 7 / OSC 9;9, None until reported |
visible_lines |
int |
Visible height in rows, O(1) (prefer over len(display)) |
visible_columns |
int |
Visible width in columns, O(1) |
history_size |
int |
Current scrollback line count |
scrollback_lines |
int |
Scrollback capacity |
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=strikethrough
Winsize
Terminal 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)
Error Types
| Exception | Inherits | Raised On |
|---|---|---|
PtyError |
Exception |
PTY open/operation failures, platform errors, read timeouts |
ProcessError |
PtyError |
Spawn/kill failures, process not running |
IOError |
PtyError |
I/O errors, winsize failures |
EOF note: after the child exits, read()/read_timeout() return b""
instead of raising; natively the condition is PtyErrorKind::Eof, surfaced
as an OSError with errno == 0 and a stable kind == "eof" attribute.
Architecture
High-Level Overview
┌─────────────────────────────────────────────────────────────────┐
│ 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 │ │ │ │
│ │ │ └──────────────────┘ └──────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
PTY Backend
POSIX (platform_unix.rs)
| 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 std::thread polls waitpid(WNOHANG) every 50ms (immune to Tokio starvation) |
| 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, SIGQUIT, 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,
}
}
Windows (platform_windows.rs)
| Component | Detail |
|---|---|
| ConPTY loading | Dynamic GetProcAddress from kernel32.dll (spawn raises PtyError on Windows without ConPTY — no fallback) |
| 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.
Terminal Emulation (terminal/)
Embedded from pyte_rs. Provides VT100/VT220/xterm-compatible rendering.
ANSI Parser (ansi_parser.rs)
ECMA-48 state machine with 14 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 |
DcsParam |
0–? after ESC P |
Hook byte, intermediates |
DcsIntermediate |
–/ after DCS params |
Hook byte |
DcsIgnore |
Malformed DCS bytes | ST, ESC, CAN, SUB |
DcsPassthrough |
After hook | \x9c, ESC, SUB, CAN |
SosPmApcString |
ESC X / ^ / _ (SOS/PM/APC) |
ST |
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.
Screen Buffer (screen.rs)
| 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 |
| Shell cwd | OSC 7 / OSC 9;9 tracking, survives alt-screen and reset |
| Dirty tracking | BTreeSet<usize> peek (dirty()) + drain (take_dirty_rows()) + ordered event log (poll_events()) + bell flag (take_bell()) |
| Wrap flags | Per-row soft-wrap tracking (content-flow semantics — travel with rows, clear on line-unit replace) |
| Reflow | Column resize reflows history ++ buffer as one logical sequence (wide glyphs never split, trailing blanks trimmed, bottom-anchored repack, logical cursor anchor); alt-screen conforms only |
| Background color erase | Erased cells keep the current SGR (EL/ED/ECH/ICH/DCH/IL/DL, scrolling); full reset and DECALN still use defaults |
Scrollback (history.rs)
| 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 |
GIL Management Strategy
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 released across .await: async reads/writes run on tokio threads
without the GIL; it is reacquired to hand results back to Python.
GIL is held by:
- Converting
Vec<u8>→PyBytes - Raising
PyErrexceptions - Synchronous
TerminalStateemulator calls (feed,display,styled_viewport, …) — parsing and screen-building run under the GIL
Memory Safety
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 |
Examples
Simple Command Execution
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())
Interactive Shell
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())
pexpect-Style Expect
import asyncio
from stitch_pty import spawn
async def main():
import re
session = await spawn("bash", ["-i"])
# Wait for a prompt (single literal → index 0)
prompt = await session.expect(b"$ ", timeout=10.0)
print(f"Got: {bytes(prompt).decode()}")
# Send command
await session.write(b"uname -a\n")
# Wait for whichever comes first: output line or an error banner.
# Lists match in list order; regex hits carry a match object.
output = await session.expect(
[re.compile(rb"Linux .*"), b"command not found"], timeout=5.0
)
print(f"Output [{output.index}]: {output.buffer.decode()}")
if output.match is not None:
print(f"Matched: {output.match.group(0).decode()}")
await session.terminate()
asyncio.run(main())
expect() returns an ExpectResult(index, match, buffer): index is the
winning pattern's position, match is the regex match (or None for literal
hits), and buffer holds everything read so far. Timeouts and EOF raise
TimeoutError with the bytes seen attached as .buffer. Legacy call sites
keep working: b"…" in result, result == b"…", and bytes(result) all
read from buffer — only .decode() needs spelling out now.
Raw PTY (No Child)
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())
PySide6 Terminal Emulator (GUI)
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 100
Key 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
Building from Source
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.
Prerequisites
| 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 maturin
Building as a Rust Crate
To 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.9.0"
To build and test the pure Rust code locally:
cargo build
cargo test
Building as a Python Extension
The 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/opticsWolf/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
Cross-Compilation
# 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-gnu
Source Projects & Dependencies
stitch-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.
1. portable-pty — PTY Abstraction Layer
| 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
2. pyte — Terminal Emulation
| 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
3. vte — ANSI Escape Sequence Parser
| 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 — 14-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)
Dependency Summary
| 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 Support
| Platform | Backend | Signal Support | Resize Signal | Exit Info | Status |
|---|---|---|---|---|---|
| Linux | POSIX openpty() + fork() |
Full | ✅ SIGWINCH |
code + signal | ✅ Complete |
| macOS | POSIX openpty() + fork() |
Full | ✅ SIGWINCH |
code + signal | ✅ Complete |
| Windows 10 1809+ | ConPTY + NamedPipes | Ctrl+C, TERM/KILL (terminate) | ❌ No signal | code only | ✅ Complete |
| Windows < 1809 | — (spawn raises PtyError) |
— | — | — | ❌ Unsupported |
ExitStatus.core_dumpedexists on all platforms but is alwaysFalse(reserved for futureWCOREDUMPreporting — the reaper currently discards the core flag).
Platform Differences
| 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, others rejected |
| Resize signal | SIGWINCH forwarded to process group via tcgetpgrp |
No signal; ResizePseudoConsole only |
| Exit info | exit_code + signal (core_dumped always False, reserved) |
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 |
License
MIT OR Apache-2.0
Metadata
Release files for stitch-pty 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| stitch_pty-0.9.1-cp312-abi3-win_amd64.whl | CPython 3.12 | abi3 | Windows x86-64 | Details |
| stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_x86_64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_aarch64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| stitch_pty-0.9.1-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.12 | abi3 | macOS 10.12+ x86-64, macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64) | Details |
Total release size: 2.7 MB
Release files / stitch_pty-0.9.1-cp312-abi3-win_amd64.whl
| Download URL | stitch_pty-0.9.1-cp312-abi3-win_amd64.whl |
|---|---|
| Size | 540.5 kB |
| Tags | CPython 3.12 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
3cdcbaf631604f76ea694ed6c53c590de27e255295126c6518fa8723f21fc0b7
|
|
BLAKE2b-256 checksum How to use checksums |
6b3c52ca221832e63ebbcd34889a23c2dd51cbcae872d16aaf83d6c28cd7e9f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_x86_64.whl
| Download URL | stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 560.2 kB |
| Tags | CPython 3.12 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2afc6097bbc392ab846fbdf15475b68f9734d9ef771c2a89d9a21f4017feefc0
|
|
BLAKE2b-256 checksum How to use checksums |
25a602f87e2ca29c262cd2f0ae2fe17eb5faec24dadd90b5813dd3dd5e16d2fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_aarch64.whl
| Download URL | stitch_pty-0.9.1-cp312-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 549.6 kB |
| Tags | CPython 3.12 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
5c69e8579202e46a3a4923c94c63df68e860d8797d3c3f48923a4cac5a1c852e
|
|
BLAKE2b-256 checksum How to use checksums |
a0acd87253b7820f08573bc73db63c507598c5a47cb9e8786ef62a5ec210bf9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / stitch_pty-0.9.1-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | stitch_pty-0.9.1-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 1.0 MB |
| Tags | CPython 3.12 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
b736c1ec110c42df372455087da3df4c93edead7d8189976a9a7ad127e2dc75e
|
|
BLAKE2b-256 checksum How to use checksums |
ecdf411b97c7ec3552f48ab710a934d72b2c00c3e7070fee1b554822f10e7dfd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log