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

gopty

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 9 Imported by: 0

README

gopty

GitHub go.mod Go version GitHub tag (latest SemVer) Unit-Tests Go Reference

English | 简体中文

A small, platform-neutral pty (pseudo terminal) backend for Go, plus an interactive test fixture built on top of it. Start a child process on a pty master and read/write its byte stream, resize the terminal and wait for its exit code — the same code on Linux, macOS and Windows (ConPTY).

Packages

Package Use it for
github.com/gookit/gopty Product code: Spec + Start → Pty (Read / Write / Resize / Wait / Close), IsAvailable(), IsHostBlocked(err), IsBlockedExitCode(code)
github.com/gookit/gopty/ptytest Test code: drive a program inside a pty from go test — Start / StartSpec, Mark, Expect / Contains / Count, Text (ANSI stripped) / RawText, Send / SendLine / SendCtrlC, WaitExit, and an automatic Close cleanup

Install

go get github.com/gookit/gopty
go get github.com/gookit/gopty/ptytest   # the test fixture

Requires Go 1.23 or newer.

Backends

GOOS Backend Dependency
linux, macOS, *BSD creack/pty (the master file is the stream) github.com/creack/pty
windows (Win10 1809+) a vendored ConPTY copy in ./conpty, MIT golang.org/x/sys

Both backends satisfy the same Pty contract, so a program that runs pty children compiles and behaves the same on a unix worker and on Windows.

The Windows side deliberately keeps golang.org/x/sys as its only dependency: it does not pull in hcsshim and its indirect chain the way aymanbagabas/go-pty does.

Usage

Run a child on a pty
import "github.com/gookit/gopty"

p, err := gopty.Start(gopty.Spec{Command: "bash", Args: []string{"-i"}, Cols: 100, Rows: 30})
if err != nil {
	return err
}
defer p.Close()

_, _ = p.Write([]byte("echo hi\n"))
out, _ := io.ReadAll(p)          // stdout + stderr, multiplexed
code, err := p.Wait(ctx)         // blocks until the child exits (or ctx is cancelled)

Spec fields:

Field Meaning
Command, Args executable + args (argv[0] not included). On Windows they are joined into one safely-quoted command line (windows.EscapeArg), since ConPTY takes a command line, not an argv
Env full process env; nil inherits the parent. On Windows duplicates are de-duplicated "last wins" (the os/exec rule) because CreateProcess keeps the first
Dir working directory; "" inherits
Cols, Rows initial terminal size; 0 uses the platform default

Pty contract: Read carries stdout+stderr, Write is stdin, Resize sets the window size in cells, Wait(ctx) returns the exit code (a real process exit is (code, nil), only a genuine reap failure returns an error), Close releases the master and — on unix — kills the child. Teardown order belongs to the caller's state machine; this package only provides the primitives.

Drive an interactive program from a test
import "github.com/gookit/gopty/ptytest"

func TestAskName(t *testing.T) {
	s := ptytest.Start(t, "sh", "-c", `printf "name? "; read n; echo got=$n`)
	mark := s.Mark()
	if !s.Expect(`name\?`, mark) {
		t.Fatalf("no prompt, output:\n%s", s.Text())
	}
	s.SendLine("tom")
	if !s.Expect(`got=tom`, mark) {
		t.Fatalf("no answer, output:\n%s", s.Text())
	}
}

It is the Go counterpart of a "pty smoke script" (what a Python pty driver or a Windows console harness used to provide), so the same test code runs on every platform.

Rules of thumb:

  • assert only on output produced after a Mark — earlier turns would satisfy the check by accident;
  • wait for the prompt (Expect) before typing: a program that clears pending input before asking drops too-early keystrokes;
  • Enter is "\r", Ctrl-C is 0x03 (use SendCtrlC), Ctrl-D is 0x04;
  • Text() strips ANSI escapes, RawText() keeps them (colour-only output is invisible to Text);
  • Start skips the test with a reason when no pty backend is available or the host refuses a pty child, and registers its own Close cleanup.

Platform notes

Windows (ConPTY)
  • ConPTY needs Win10 1809+; IsAvailable() probes it and ptytest.Start skips when it is missing.
  • Some Windows process contexts refuse a ConPTY child: inside a job object that does not allow breakaway, CreateProcess fails with ERROR_ACCESS_DENIED, or the child dies at load with STATUS_DLL_INIT_FAILED (0xC0000142). Use IsHostBlocked(err) / IsBlockedExitCode(code) to detect it and degrade; ptytest skips with the reason instead of reporting a bare error code. The outcome is environment-dependent — measured both ways inside the same agent job (cmd.exe started fine, another harness was blocked) — so do not assume either result.
  • When the host blocks ConPTY and you still need to test interactive prompts, drive a real console: AllocConsole, child stdin = CONIN$, then send keys with WriteConsoleInput.
  • After the child has exited on its own, Close() first drains what conhost is still forwarding, so the last lines a short-lived child printed are not discarded: ConPTY's output pipe does not report EOF while the pseudo console is open (closing it is what ends the stream), so the drain waits for the pipe to go quiet (250ms without a read) instead of for EOF, and gives up at 2s (drainGrace, the same bound the unix backend uses). A live child (cancel) is torn down at once.
  • Close() is idempotent. The vendored conpty.Close() closes four pipe handles and the pseudo console unconditionally, and a second close would release handle values Windows may have reused since (observed to take the test binary down silently); the adapter guards it with sync.Once.
  • A synthetic Ctrl-C written to the input (0x03) is not a real CTRL_C_EVENT; the child's console mode decides how it is read.
unix
  • The parent keeps its own slave fd until the child has been reaped, so the reaper's close is the last one. On darwin the BSD tty layer flushes master-unread output on the last close of the slave, which would lose everything a short-lived child printed before the reader caught up.
  • After the child has exited, Close() first waits (bounded, 2s) for the reader to reach EOF, so the output written right before exit is not discarded; a live child (cancel) is torn down at once.

Development & testing

go test ./...                  # host platform
go test -race ./...            # unix
GOOS=windows go build ./...    # cross-compile check (also GOOS=darwin, GOOS=linux)
go vet ./...

CI (.github/workflows/go.yml) runs the tests on ubuntu + windows for Go 1.23 / 1.24 / 1.25 / stable, plus macOS on 1.23 and stable, and the race detector on the ubuntu/stable leg. On a runner whose session cannot host a ConPTY child, the Windows pty cases skip and say so in the log.

Consumers

  • inhere/gofer — pty runner for interactive agent sessions (internal/runner/pty);
  • gookit/gcli — interactive end-to-end tests of prompts and shared options (e2e_pty_test.go).

License

MIT, see LICENSE. The vendored ./conpty package keeps its own copy in conpty/LICENSE.

Documentation

Overview

Package gopty is a small, platform-neutral pty (pseudo terminal) backend: it starts a child process attached to a pty master and hands the caller a raw bidirectional byte stream, plus resize and exit-code wait.

Backends (see pty_unix.go / pty_windows.go):

  • unix: github.com/creack/pty, whose master file is the stream.
  • windows: a vendored copy of UserExistsError/conpty (./conpty, MIT), Win10 1809+. ConPTY takes ONE command-line string, so Start joins Command+Args into a safely quoted line (windows.EscapeArg); a caller-supplied Env is de-duplicated with the os/exec rule ("last occurrence wins") because CreateProcess keeps the first.

Both backends satisfy the same Pty contract, so a program that runs pty children compiles and behaves the same on a unix worker and on Windows.

A ConPTY child must be created through the ConPTY API rather than os/exec (golang/go#62708), and some Windows process contexts refuse it: suga's console harness (scripts/wincons) reported CreateProcess failing with STATUS_DLL_INIT_FAILED (0xC0000142) inside a gofer job, while this repo's own probe in a gofer job starts a ConPTY child fine (cmd.exe, exit 0) — so the outcome is environment-dependent. Treat it as possible: check IsHostBlocked / IsBlockedExitCode instead of assuming either way. When it is blocked, drive a real console for interactive work instead.

Interactive (test) children are best driven through the sibling package github.com/gookit/gopty/ptytest, which adds expect/send helpers on this contract.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsAvailable

func IsAvailable() bool

IsAvailable is always true on unix: creack/pty opens /dev/ptmx at Start time and a failure surfaces there (probing here would just duplicate that).

func IsBlockedExitCode

func IsBlockedExitCode(int) bool

IsBlockedExitCode is always false on unix: a unix pty child always runs.

func IsHostBlocked

func IsHostBlocked(error) bool

IsHostBlocked is always false on unix: no platform mechanism blocks a pty child the way a Windows job object does (see the windows file).

Types

type Pty

type Pty interface {
	io.ReadWriteCloser
	// Resize sets the pty window size in character cells.
	Resize(cols, rows int) error
	// Wait blocks until the child exits (or ctx is cancelled) and returns the
	// process exit code. A ctx cancellation returns a non-nil error without
	// reaping; the caller kills via Close and calls Wait once.
	Wait(ctx context.Context) (exitCode int, err error)
}

Pty is a running child attached to a pty master. Read/Write carry the raw byte stream to/from the child (stdout+stderr multiplexed on Read, stdin on Write); Resize sets the terminal window size; Wait blocks for the child to exit and returns its exit code. Close drains the output the child wrote before exiting (bounded by drainGrace), then releases the master fd (and, on unix, kills the child) — teardown ORDER is owned by the caller's state machine, this type only provides the primitives.

func Start

func Start(spec Spec) (Pty, error)

Start runs spec under a freshly allocated pty (unix: creack/pty).

Unlike creack.Start, the parent keeps its own slave fd until the child has been reaped. On darwin the child's exit would otherwise be the LAST close of the slave, and the BSD tty layer flushes output the master has not read yet on that close — a child that printed and exited before the reader caught up lost its output, all of it for a short-lived one. Holding the slave makes the reaper's close the last one; it runs only after the exit is known, and the reader drains the buffer to EOF before Close tears the master down (see Close).

type Spec

type Spec struct {
	Command string   // executable to run
	Args    []string // args (not including argv[0])
	Env     []string // full process env (KEY=VALUE); nil = inherit os.Environ
	Dir     string   // working directory; "" = inherit
	Cols    int      // initial terminal width (columns); 0 = platform default
	Rows    int      // initial terminal height (rows); 0 = platform default
}

Spec is a platform-neutral request to start a child under a pty.

Directories

Path Synopsis
Package conpty is the vendored Windows ConPTY backend (see conpty.go, which carries the //go:build windows constraint).
Package conpty is the vendored Windows ConPTY backend (see conpty.go, which carries the //go:build windows constraint).
Package ptytest drives a program inside a pty from ordinary Go tests: start it, wait for the prompt, type, assert on the output, then shut it down.
Package ptytest drives a program inside a pty from ordinary Go tests: start it, wait for the prompt, type, assert on the output, then shut it down.

Jump to

Keyboard shortcuts

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