windrunner

package module
v0.0.0-...-5ef5517 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 12 Imported by: 2

README

windrunner

A persistent terminal session engine for Go — the layer between spawning a PTY and shipping a product.

Windrunner owns pseudo-terminals, runs an authoritative terminal emulator for each one (pure Go, no cgo, no WASM), and keeps them alive in a daemon that outlives any client. Attach from anywhere and get an exact snapshot — screen, scrollback, cursor — followed by the live byte stream. Detach, come back tomorrow, it's all still there.

It is the engine inside tools like tmux, screen, and every terminal-tab UI — extracted, embeddable, and unopinionated about what you build on top.

What it is

  • A library first. windrunner is an importable engine: spawn sessions, write input, resize, subscribe to output, snapshot state. The daemon and wire protocol are packages, not requirements — embed the engine in-process if that's all you need.
  • Authoritative emulation. Each session's terminal state lives in a real VT emulator (charmbracelet/x/vt), fed directly from the PTY. Programs that query their terminal (cursor position, colors, capabilities) get real answers. Reattach snapshots are exact serializations of that state — not lossy screen scrapes, not replay logs.
  • Bytes on the wire. Clients receive a snapshot, then raw output bytes, and send raw input bytes back. Bring any front end: a Go TUI, xterm.js in a browser, a recorder, a bot. The client dials through a transport you supply — a unix socket by default, anything ordered and full-duplex otherwise — so a daemon on the far side of an SSH bridge is the same daemon to every call.
  • Slow clients repaint instead of dying. A client that cannot keep up with a burst is normally dropped, because an unbounded backlog is worse. One that attaches with Resync is handed the terminal's exact state at the moment it fell behind and keeps streaming from there — the bytes it missed are gone, but its replica is correct. That is the right trade for anything that watches (a browser terminal, a wall of live panes) and the wrong one for anything that must see every byte, so it is a choice.
  • Sessions carry opaque metadata. Tag sessions with whatever your product means by them — task, branch, agent, owner. Windrunner stores and returns the bag; it never interprets it.
  • Sessions can talk — deliberately. windrunner ls, peek <id> (print a session's rendered screen), and send <id> text... (type text plus Enter) are a control plane any program with a shell can drive: discover peers, read their screens, prompt them. Speaking is opt-in per session (new -peer) — writing into a session's stdin is code execution as far as its process is concerned — and the daemon audit-logs every send, attributed via the WINDRUNNER_SESSION ID each child is spawned with. No stream-splicing: listening is snapshots, speaking is input. For coordination beyond poke-and-peek, windrunner events streams the daemon's pub/sub feed — session lifecycle plus idle/busy transitions — so a peer can wait for a session to go quiet instead of polling its screen.

What it is not

  • Not a terminal multiplexer for humans: no panes, no layouts, no copy mode, no keybindings, no status bar. Those belong to the products built on top.
  • Not a renderer. Windrunner never draws; it maintains state and streams bytes.
  • Not an orchestrator. It doesn't know what an "agent" is — or a "task", or a "workspace". Sessions are processes on PTYs with metadata you define.

Status

Early 0.x. The API is settling against its first consumer (stormlight) and will break as it does. Unix only for now (macOS and Linux).

License

MIT

Documentation

Overview

Package windrunner is a persistent terminal session engine: it owns pseudo-terminals, runs an authoritative VT emulator for each one, and hands exact state snapshots plus live byte streams to any number of attached clients.

The engine is deliberately unopinionated. It knows processes, terminals, and opaque metadata — never panes, layouts, agents, or rendering. Products are built on top of it, not inside it.

Index

Constants

View Source
const DefaultIdleAfter = 2 * time.Second

DefaultIdleAfter is how long a session's terminal must stay quiet before it is reported idle, when SpawnSpec does not say otherwise.

View Source
const DefaultScrollback = 2000

DefaultScrollback bounds each session's emulator history when SpawnSpec does not say otherwise.

Variables

This section is empty.

Functions

This section is empty.

Types

type AttachOptions

type AttachOptions struct {
	// Buffer is the subscription's message buffer; 0 means 64.
	Buffer int
	// Resync trades byte-exact replay for staying attached. A subscriber
	// that fills its buffer is normally dropped (see Lagged); one that
	// asked to resync instead has its backlog replaced by a Message
	// carrying the terminal's exact state at the moment it fell behind,
	// and keeps streaming from there.
	//
	// This is the right trade for a viewer and the wrong one for a
	// recorder: a replica that repaints ends up correct, while a transcript
	// with a hole in it does not. Watching clients — a browser terminal,
	// a wall of live panes — should ask for it; anything that must see
	// every byte should not.
	Resync bool
}

AttachOptions configures a subscription. The zero value is the historical behavior: the default buffer, dropped on lag.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is a registry of live sessions. It can be embedded directly in a process, or served over a socket by the server package; the API is the same either way.

func NewEngine

func NewEngine() *Engine

func (*Engine) Close

func (e *Engine) Close()

Close removes every session, ends every event subscription, and refuses further spawns.

func (*Engine) Remove

func (e *Engine) Remove(id string)

Remove ends a session completely: process, PTY, terminal state, and subscribers. This is the only way session state is discarded — exit alone keeps it attachable.

func (*Engine) Session

func (e *Engine) Session(id string) (*Session, bool)

Session finds a live session by ID.

func (*Engine) Sessions

func (e *Engine) Sessions() []*Session

Sessions lists every session, oldest ID first for stable output.

func (*Engine) Spawn

func (e *Engine) Spawn(spec SpawnSpec) (*Session, error)

Spawn starts a process on a PTY of its own and begins recording its terminal state.

func (*Engine) Subscribe

func (e *Engine) Subscribe(buffer int) *EventSubscription

Subscribe returns a feed of engine events from this moment on. A subscriber that stops reading is dropped, not waited for (see EventSubscription.Lagged). For any one session, its spawned event is published before every other event about it; beyond that, ordering across concurrent transitions is best-effort.

type Event

type Event struct {
	Type      EventType
	SessionID string
	Command   string // EventSpawned
	ExitCode  int    // EventExited
}

Event is one entry in the engine's pub/sub stream: a session appeared, went quiet, spoke again, exited, or was removed. Structured signals for coordination — richer than poke-and-peek, still not byte-splicing.

type EventSubscription

type EventSubscription struct {
	// contains filtered or unexported fields
}

EventSubscription is one reader of the engine's event stream.

func (*EventSubscription) Close

func (sub *EventSubscription) Close()

Close ends the subscription. Safe to call at any time, any number of times.

func (*EventSubscription) Events

func (sub *EventSubscription) Events() <-chan Event

Events yields engine events. The channel closes when the subscription is closed, the engine closes, or the reader falls too far behind.

func (*EventSubscription) Lagged

func (sub *EventSubscription) Lagged() bool

Lagged reports whether the subscription was dropped for reading too slowly. The recovery is to subscribe again and re-list sessions: fresh state is always cheaper than an unbounded backlog.

type EventType

type EventType string

EventType names one kind of daemon-observed change.

const (
	EventSpawned EventType = "spawned"
	EventExited  EventType = "exited"
	EventRemoved EventType = "removed"
	// EventIdle and EventBusy report output activity: a session is busy
	// while its terminal is producing output and idle once it has been
	// quiet for its idle window. Sessions start busy; the first event is
	// always an idle.
	EventIdle EventType = "idle"
	EventBusy EventType = "busy"
)

type Message

type Message struct {
	Bytes  []byte
	Resize *Resize
	// Resync, when set, supersedes everything the subscriber had not read
	// yet: the reader fell a whole buffer behind, so the backlog was
	// dropped in favor of this exact state (see AttachOptions.Resync).
	// Bytes is nil and later messages continue from here, so a replica
	// applies it by replacing itself wholesale rather than appending.
	//
	// Its ANSI is read-only: one serialization is shared by every
	// subscriber owed state at that moment, so rewriting it in place
	// corrupts the others. Copy before modifying.
	Resync *Snapshot
}

Subscription is one attached reader of a session's output stream. Message is one delivery on a subscription: terminal output bytes, and — when the session's terminal moved under the subscriber — the new size, carried with the repaint those bytes hold so a replica resizes and repaints as one ordered step.

type Resize

type Resize struct {
	Cols, Rows int
}

Resize is a terminal size change riding the stream.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is one process on one PTY, with the authoritative record of what its terminal looks like. All methods are safe for concurrent use.

func (*Session) Alive

func (s *Session) Alive() bool

Alive reports whether the child process is still running.

func (*Session) Attach

func (s *Session) Attach(buffer int) (Snapshot, *Subscription)

Attach returns the session's state right now and a subscription that carries every output byte after it — atomically, so nothing falls in the gap between snapshot and stream. Shorthand for AttachWith of a buffer.

func (*Session) AttachWith

func (s *Session) AttachWith(options AttachOptions) (Snapshot, *Subscription)

AttachWith is Attach with a policy: see AttachOptions.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

Done closes when the child exits. The session's terminal state remains attachable afterward until Close.

func (*Session) ExitCode

func (s *Session) ExitCode() int

ExitCode is meaningful once Done is closed.

func (*Session) ID

func (s *Session) ID() string

ID names the session; stable for its whole life.

func (*Session) Kill

func (s *Session) Kill() error

Kill forcibly ends the child process. Terminal state survives until Close.

func (*Session) Metadata

func (s *Session) Metadata() map[string]string

Metadata returns a copy of the session's opaque tag bag.

func (*Session) Peer

func (s *Session) Peer() bool

Peer reports whether the session opted into peer input at spawn. Immutable for the session's life — a guardrail that could be flipped after the fact would not be one.

func (*Session) Resize

func (s *Session) Resize(cols, rows int) error

Resize moves the PTY and the emulator together; the child learns via SIGWINCH (and in-band reports, if it asked for them).

func (*Session) SetMetadata

func (s *Session) SetMetadata(metadata map[string]string)

SetMetadata replaces the session's metadata wholesale.

func (*Session) Size

func (s *Session) Size() (cols, rows int)

Size reports the terminal's current dimensions.

func (*Session) Snapshot

func (s *Session) Snapshot() Snapshot

Snapshot serializes the terminal's exact current state.

func (*Session) Title

func (s *Session) Title() string

Title reports the most recent title the program set (OSC 0/2), or "".

func (*Session) Write

func (s *Session) Write(p []byte) (int, error)

Write delivers input bytes to the child, exactly as typed.

type Snapshot

type Snapshot struct {
	Cols, Rows int
	ANSI       []byte
}

Snapshot is a session's exact terminal state, serialized. Writing ANSI to a fresh Cols x Rows terminal reproduces the scrollback, the screen, and the cursor position.

type SpawnSpec

type SpawnSpec struct {
	Command string
	Args    []string
	Dir     string
	// Env is the child's environment; nil inherits the engine process's.
	// TERM is set to xterm-256color unless the caller provides one — the
	// child must describe output for the terminal windrunner actually
	// emulates. WINDRUNNER_SESSION is always this session's own ID, and
	// any inherited one is replaced, so a session's process can name
	// itself to the control plane and cannot name someone else.
	Env []string
	// EnvOverride is applied on top of Env — or on top of the inherited
	// environment when Env is nil — replacing any entry that names the
	// same variable. It is how a caller adds a few variables without
	// having to supply the whole environment, which a client on another
	// machine cannot meaningfully do: its own environ describes a
	// different host, and replacing the daemon's would leave the child
	// without PATH, HOME, or anything else it was going to need.
	EnvOverride []string
	Cols, Rows  int
	// Scrollback caps emulator history in lines; 0 means
	// DefaultScrollback.
	Scrollback int
	// IdleAfter is the quiet window behind idle/busy events: no output for
	// this long means idle. 0 means DefaultIdleAfter.
	IdleAfter time.Duration
	// Peer opts the session into peer input: writes to its stdin that
	// arrive without an attachment, the way automation and other sessions
	// speak. The engine only stores the bit — Write never checks it; the
	// server package refuses the detached input op for sessions that did
	// not opt in.
	Peer bool
	// Metadata travels with the session verbatim. The engine stores and
	// returns it; it never reads it.
	Metadata map[string]string
}

SpawnSpec describes a session to start. Cols and Rows are required; the zero values of everything else are usable defaults.

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

func (*Subscription) Close

func (sub *Subscription) Close()

Close detaches the subscription. Safe to call at any time, any number of times.

func (*Subscription) Lagged

func (sub *Subscription) Lagged() bool

Lagged reports whether the subscription was dropped for reading too slowly. The recovery is to attach again: a fresh snapshot is always cheaper than an unbounded backlog. A subscription that asked to resync is never dropped for lag, so this is always false for one.

func (*Subscription) Output

func (sub *Subscription) Output() <-chan Message

Output yields the stream's messages in terminal order. The channel closes when the session ends, the subscription is closed, or the reader falls too far behind (see Lagged).

Directories

Path Synopsis
Package client speaks the wire protocol to a windrunner daemon: control calls over one connection, attachments over one dedicated connection each.
Package client speaks the wire protocol to a windrunner daemon: control calls over one connection, attachments over one dedicated connection each.
cmd
windrunner command
Command windrunner is the reference client and daemon: persistent terminal sessions you can create, list, and attach to — a minimal screen alternative that doubles as a demo of the library.
Command windrunner is the reference client and daemon: persistent terminal sessions you can create, list, and attach to — a minimal screen alternative that doubles as a demo of the library.
Package server exposes an Engine over a listener, speaking the wire protocol.
Package server exposes an Engine over a listener, speaking the wire protocol.
Package wire is windrunner's socket protocol: length-prefixed frames over a unix socket.
Package wire is windrunner's socket protocol: length-prefixed frames over a unix socket.

Jump to

Keyboard shortcuts

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