Help us shape the future of Go! Take the 2026 Go Developer Survey

ptyx

package module
v0.2.1-0...-2a30ff4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 14 Imported by: 0

README

ptyx — Cross-Platform PTY/TTY Toolkit

ptyx logo

CI Status Go Go Reference Platform


Overview

ptyx is a Go library that provides a simple, cross-platform API for managing pseudo-terminals (PTY) and terminal TTYs.

Features

  • Cross-Platform PTY: Simple API to spawn processes in a pseudo-terminal on macOS, Linux, BSDs (using ptmx) and on Windows (using ConPTY).
  • TTY Control: Functions to control the local terminal, including setting raw mode, getting terminal size, and receiving resize notifications.
  • I/O Bridge: A Mux utility to easily connect the local terminal's stdin/stdout to the PTY session.
  • Zero External Dependencies: Relies only on the standard library and the official golang.org/x packages (sys, term).

How It Works

ptyx acts as a bridge between a user's terminal and a pseudo-terminal (PTY), enabling interactive command-line applications. It abstracts away the platform-specific details of PTY/TTY management.

ptyx architecture diagram

The architecture consists of three main components:

  • User Interfaces (Console): This represents the user's actual terminal. ptyx captures input from its stdin (Pipe In) and writes output to its stdout (Pipe Out).

  • Interface Handlers (ptyx): The core of the library, which manages the communication pipes. It ensures that data flows smoothly between the user's console and the underlying pseudo-terminal.

  • Pseudoterminal (Backend): The OS-level PTY implementation that runs the child process.

    • On Windows, this is the ConPTY API.
    • On Unix-like systems (macOS, Linux), this is a traditional /dev/ptmx device.

Installation

go get github.com/safedep/ptyx

Run the demos

# Interactive shell
go run ./cmd/shell

# Local spinner/progress (no PTY)
go run ./cmd/spinner

# Show terminal color support
go run ./cmd/color

# Test ANSI color passthrough by running the color demo in a PTY
go run ./cmd/passthrough

# Raw stdin echo
go run ./cmd/echo

# Capture and parse terminal output as events
go run ./cmd/event

# Send input to a program waiting in a PTY
go run ./cmd/scan

# Run a sequence of commands in a shell and handle interruption
go run ./cmd/sequence

# Run a shell inside a PTY, which itself runs inside a PTY
go run ./cmd/nested

# Resize bridge
go run ./cmd/resize -- /bin/sh

# Run an arbitrary command in a PTY
go run ./cmd/run -- bash -lc "echo hi; read -p 'press:' x; echo done"

Use as a library

ptyx is designed to be simple to use. Here are a few examples showing how to accomplish common tasks.

1. Spawning a Non-Interactive Process

This is the most basic use case: running a command in a pseudo-terminal and streaming its output.

package main

import (
	"context"
	"io"
	"log"
	"os"

	"github.com/safedep/ptyx"
)

func main() {
	// Spawn a command in a new PTY session.
	// A context is used for cancellation.
	s, err := ptyx.Spawn(context.Background(), ptyx.SpawnOpts{
		Prog: "ping",
		Args: []string{"8.8.8.8"},
	})
	if err != nil {
		log.Fatalf("spawn failed: %v", err)
	}
	// Ensure the session is closed to clean up resources.
	defer s.Close()

	// Stream the PTY output to standard out.
	go io.Copy(os.Stdout, s.PtyReader())

	// Wait for the process to exit.
	if err := s.Wait(); err != nil {
		log.Printf("process wait failed: %v", err)
	}
}

2. Creating a Full Interactive Shell

For interactive applications like a terminal emulator, you need to connect the user's TTY to the PTY session. ptyx makes this easy.

The following example creates a complete, cross-platform interactive shell.

package main

import (
	"context"
	"fmt"
	"log"
	"runtime"

	"github.com/safedep/ptyx"
)

func main() {
	// 1. Get a handle to the local console/TTY.
	c, err := ptyx.NewConsole()
	if err != nil {
		log.Fatalf("failed to create console: %v", err)
	}
	defer c.Close()

	// 2. Enable virtual terminal processing for color support (especially on Windows).
	c.EnableVT()

	// 3. Set the TTY to raw mode to pass all key presses directly to the PTY.
	st, err := c.MakeRaw()
	if err == nil {
		defer c.Restore(st)
	}

	// 4. Get the initial terminal size.
	w, h := c.Size()
	shell := "sh"
	if runtime.GOOS == "windows" {
		shell = "powershell.exe"
	}

	// 5. Spawn the shell in a new PTY with the correct dimensions.
	s, err := ptyx.Spawn(context.Background(), ptyx.SpawnOpts{Prog: shell, Cols: w, Rows: h})
	if err != nil {
		log.Fatalf("failed to spawn: %v", err)
	}
	defer s.Close()

	// 6. Create a multiplexer to bridge I/O between the local TTY and the PTY.
	m := ptyx.NewMux()
	if err := m.Start(c, s); err != nil {
		log.Fatalf("failed to start mux: %v", err)
	}
	defer m.Stop()

	// 7. Handle terminal resize events.
	go func() {
		for range c.OnResize() {
			_ = s.Resize(c.Size())
		}
	}()

	// 8. Wait for the PTY session to end.
	if err := s.Wait(); err != nil {
		if exitErr, ok := err.(*ptyx.ExitError); ok {
			fmt.Printf("\nProcess exited with code %d\n", exitErr.ExitCode)
		}
	}
}

3. Cancelling a Process

You can gracefully terminate a PTY session by cancelling its context. A common use case is handling user interruptions (e.g., Ctrl+C).

package main

import (
	"context"
	"io"
	"log"
	"os"
	"os/signal"

	"github.com/safedep/ptyx"
)

func main() {
	// Create a context that is cancelled when an interrupt signal is received.
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	log.Println("Spawning 'ping' process. Press Ctrl+C to terminate.")
	s, err := ptyx.Spawn(ctx, ptyx.SpawnOpts{Prog: "ping", Args: []string{"8.8.8.8"}})
	if err != nil {
		log.Fatalf("spawn failed: %v", err)
	}
	defer s.Close()

	go io.Copy(os.Stdout, s.PtyReader())

	// Wait for the process to exit. This will block until the context is cancelled.
	if err := s.Wait(); err != nil {
		log.Printf("Process terminated: %v", err)
	}
}

API References

type Console interface {
  In() io.Reader
  Out() io.Writer
  Err() *os.File
  IsATTYOut() bool
  Size() (int, int)
  MakeRaw() (RawState, error)
  Restore(RawState) error
  EnableVT()
  OnResize() <-chan struct{}
  Close() error
}

type Session interface {
  PtyReader() io.Reader
  PtyWriter() io.Writer
  Resize(cols, rows int) error
  Wait() error
  Kill() error
  Close() error
  Pid() int
  CloseStdin() error
}

type Mux interface {
  Start(c Console, s Session) error
  Stop() error
}

type SpawnOpts struct {
  Prog string
  Args []string
  Env  []string
  Dir  string
  Cols int
  Rows int
}

type ExitError struct {
  ExitCode int
}

type RawState interface{}

Notes

  • Unix/macOS/WSL: full PTY support using openpty or /dev/ptmx.
  • Windows: Full ConPTY session support, console VT, and resize.

Trade-offs

Raw mode keeps output newline translation enabled

MakeRaw switches the console into raw mode so key presses pass straight through to the PTY, but unlike a plain term.MakeRaw it keeps output post-processing (OPOST/ONLCR) enabled. A bare \n written directly to the terminal is therefore still translated to \r\n, so log lines, spinners, and prompts that the host program prints while raw mode is active start at the left edge instead of "staircasing" diagonally down the screen.

The trade-off is that raw mode is not byte-transparent on output:

  • Line-oriented output is unaffected. A child program's PTY output already arrives \r\n-terminated, so re-translation is a harmless no-op.
  • Full-screen TUI children may render incorrectly. Apps like vim, htop, or less emit bare \n as literal cursor movement; when their output is copied straight to this terminal, the added \r can shift the cursor unexpectedly.

Input handling, Ctrl-C, echo suppression, and Restore are unaffected — only the output newline translation differs.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCmdLineUnsupported = errors.New("ptyx: CmdLine is supported on windows only")

ErrCmdLineUnsupported reports a non-empty CmdLine on a platform that has no Windows command line. Spawn fails instead of ignoring the field.

View Source
var ErrCmdLineWithArgs = errors.New("ptyx: CmdLine and Args are mutually exclusive")

ErrCmdLineWithArgs reports SpawnOpts that carry both CmdLine and Args. Honouring one would silently drop the other.

View Source
var ErrMuxAlreadyStarted = errors.New("ptyx: mux already started")
View Source
var ErrNotAConsole = errors.New("ptyx: not a console")

Functions

func CSI

func CSI(seq string) string

func CUP

func CUP(row, col int) string

func IsErrNotAConsole

func IsErrNotAConsole(err error) bool

func Run

func Run(ctx context.Context, opts SpawnOpts) error

func RunInteractive

func RunInteractive(ctx context.Context, opts SpawnOpts) error

func SGR

func SGR(codes ...int) string

Types

type Console

type Console interface {
	In() io.Reader
	Out() io.Writer
	Err() *os.File
	IsATTYOut() bool
	Size() (int, int)
	MakeRaw() (RawState, error)
	Restore(RawState) error
	EnableVT()
	OnResize() <-chan struct{}
	Close() error
}

func NewConsole

func NewConsole() (Console, error)

type ExitError

type ExitError struct {
	ExitCode int
	// contains filtered or unexported fields
}

func (*ExitError) Error

func (e *ExitError) Error() string

func (*ExitError) Sys

func (e *ExitError) Sys() any

type Mux

type Mux interface {
	Start(c Console, s Session) error
	Stop() error
}

func NewMux

func NewMux() Mux

type RawState

type RawState interface{}

type Session

type Session interface {
	PtyReader() io.Reader
	PtyWriter() io.Writer
	Resize(cols, rows int) error
	Wait() error
	Kill() error
	Close() error
	Pid() int
	CloseStdin() error
}

func Spawn

func Spawn(ctx context.Context, opts SpawnOpts) (sess Session, err error)

type SpawnOpts

type SpawnOpts struct {
	Prog string
	Args []string
	Env  []string
	Dir  string
	Cols int
	Rows int

	// CmdLine is the raw Windows command line for the child. Spawn passes it
	// to CreateProcess without any escaping, so the caller owns every quote.
	// Set it only when the CommandLineToArgvW rules that Args follows are the
	// wrong rules, for example to start a batch file through cmd.exe.
	//
	// Windows only. Every other platform rejects a non-empty CmdLine, so a
	// caller fails loudly instead of getting a different result per platform.
	// CmdLine and Args are mutually exclusive.
	CmdLine string

	// InheritCursor starts the ConPTY at the cursor position of the host
	// terminal. Without it, ConPTY clears the screen on its first paint, and
	// Windows Terminal moves the visible output into the scrollback.
	//
	// ConPTY asks for the position with a DSR (`ESC[6n`) on the PTY output.
	// The caller must copy the terminal reply from its stdin to PtyWriter.
	// An older ConPTY waits for the reply with no timeout.
	//
	// Windows only. Other platforms ignore it, because a Unix PTY does not
	// clear the screen when it starts.
	InheritCursor bool
}

Directories

Path Synopsis
cmd
color command
echo command
event command
nested command
passthrough command
resize command
run command
scan command
sequence command
shell command
spinner command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL