Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ptyx — Cross-Platform PTY/TTY Toolkit

ptyx logo

CI Status Go Go Reference License: MIT 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.

About

Cross-platform TTY utilities (macOS, Linux, Windows, WSL)

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages