Skip to main content

stitch-pty

CI PyPI crates.io Python Rust License

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
Zero GIL contention ✅ PyO3 native async ✅ ✅
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


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 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(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
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() → 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

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
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=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 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,
    }
}

Windows (platform_windows.rs)

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.

Terminal Emulation (terminal/)

Embedded from pyte_rs. Provides VT100/VT220/xterm-compatible rendering.

ANSI Parser (ansi_parser.rs)

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.

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())

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 acquired only for:

  1. Converting Vec<u8> → PyBytes
  2. Raising PyErr exceptions

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.5.5"

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/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

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:

  • ChildKiller trait — deadlock-free kill/wait separation pattern (cloneable killer split from ChildBackend to avoid blocking .wait threads)
  • ExitStatus / ProcessExit structs — unified exit code + signal representation
  • PtySize → Winsize — window size struct with rows/cols/pixels
  • close_random_fds() — macOS Big Sur / Linux FD leak prevention (closes all FDs > 2 via /dev/fd listing)
  • ArgvQuote algorithm — proper Windows command-line quoting (backslash/quote escaping)
  • SafeAttributeList pattern — Win32 PROC_THREAD_ATTRIBUTE_LIST lifecycle management (Initialize → Update → Delete) for ConPTY attachment
  • ProcessSignaller — cross-platform signal delivery abstraction

What changed:

  • Rewritten for tokio async (portable-pty uses blocking I/O)
  • Replaced nix 0.28 → nix 0.31 with additional features (signal, poll, ioctl)
  • Replaced winapi → windows crate (0.62, modern MSFT bindings)
  • Replaced anyhow → thiserror for zero-cost error propagation
  • Added Tokio NamedPipes for Windows async I/O (portable-pty uses spawn_blocking)
  • Added dynamic ConPTY loading via GetProcAddress for 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 tracking
  • Char cell — data, fg/bg colors, bold/dim/italics/underscore/blink/reverse/hidden/strikethrough
  • Cursor — position, attributes, save/restore stack
  • Margins — scroll region top/bottom
  • CursorStyle — 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-width for 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() and absolute_cursor() for scroll-aware positioning
  • Added unicode-segmentation for 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 — 10-state ANSI parser (Ground, CsiEntry, CsiParam, CsiIntermediate, CsiIgnore, OscString, DcsEntry, DcsPassthrough, Escape, EscapeIntermediate)
  • Parser struct — state, intermediates, params, UTF-8 partial buffer, OSC raw buffer
  • Params struct — sub-parameter groups with MAX_PARAMS = 32 limit
  • Perform trait — 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 Perform impl
  • Perform trait extended with esc_dispatch for legacy ESC sequences
  • CSI parameter parsing: : treated as param separator (not subparam like upstream vte)
  • Added escape.rs designator constants (RIS, IND, NEL, HTS, etc.)
  • Added charsets.rs DEC Special line drawing map
  • Added control.rs C0/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 + 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 ⚠️ Fallback

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
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

License

MIT OR Apache-2.0

Metadata

Release files for stitch-pty 0.7.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for stitch-pty 0.7.6
File
stitch_pty-0.7.6-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.28+ x86-64 Details
stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64 Details
stitch_pty-0.7.6-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.6 MB

Release files / stitch_pty-0.7.6-cp312-abi3-win_amd64.whl

Download URL stitch_pty-0.7.6-cp312-abi3-win_amd64.whl
Size 528.1 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
2d82eee2a8b35435d70727bd79ffbc819068d5e092b7334cacb01749649789d5
BLAKE2b-256 checksum
How to use checksums
1015ddc8795695cef6125fcc43833b22da813c344161ea4a1311853d16135487
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 14, 2026.

Transparency log

Release files / stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_x86_64.whl

Download URL stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_x86_64.whl
Size 549.9 kB
Tags CPython 3.12 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
8986719a96fd7ede8642c36e07b0b9e4fb73d56824f647647aaf940bce060968
BLAKE2b-256 checksum
How to use checksums
42a7422455e2e4285ee0e38c8e8256e9dbc47664a9b94c9b40a03b37a28f908b
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 14, 2026.

Transparency log

Release files / stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_aarch64.whl

Download URL stitch_pty-0.7.6-cp312-abi3-manylinux_2_28_aarch64.whl
Size 538.7 kB
Tags CPython 3.12 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
06747498be01b9bb9b099e457d754b78f2faef627b45ecd597e202a5d44a3e86
BLAKE2b-256 checksum
How to use checksums
8b699fc8edf852b44f3e0aaf996e90a78207beffd637d8e3e3de8fc3d25b58ce
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 14, 2026.

Transparency log

Release files / stitch_pty-0.7.6-cp312-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl

Download URL stitch_pty-0.7.6-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
152f64f045f986ba4cc188f0de38d51e58c4dc104ccda6fc131fed80e93dadf5
BLAKE2b-256 checksum
How to use checksums
4115d1e2a3e737b823e869410eb87fd005dc69f0a960eb0d569f86a7ef834ac8
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 14, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.1

4 release files

0.8.1

4 release files

0.8.0

4 release files

This release

0.7.6 This release

4 release files

0.5.6

4 release files

0.5.5

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page