ghostline

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 23 Imported by: 0

README

ghostline

ghostline is an embeddable server-side terminal engine for Go. It owns real pseudo-terminals, keeps sessions running independently of attached clients, stores raw output in append-only spools, and renders complete terminal replays with libghostty-vt.

It provides terminal mechanics rather than a transport or UI. Applications can build a local multiplexer, remote shell service, development agent, browser terminal, or their own session protocol on top. Warren is one consumer, not a required integration model.

Capabilities

  • Multiple process-owned PTY sessions with input and resize support
  • Ghostty-compatible VT state, scrollback, colors, alternate screens, and synchronized output
  • Raw append-only output subscriptions with resumable byte offsets
  • Atomic checkpoints that pair a full VT replay with its exact spool boundary
  • Detached-mode terminal query responses for TUIs
  • Structured Manager and Session APIs plus the original name-based PTY compatibility API

Sessions outlive client connections, but they remain children of the embedding process. They do not survive that process exiting. A separate long-running daemon can provide persistence across client restarts.

Quick Start

manager, err := ghostline.New(ghostline.Options{
    OutputDir: "/var/lib/my-app/terminals",
    DefaultSize: ghostline.Size{Columns: 120, Rows: 36},
})
if err != nil {
    return err
}
defer manager.Close()

session, err := manager.Start(ctx, ghostline.SessionOptions{
    Name:      "build-shell",
    Directory: "/path/to/worktree",
    Command:   "bash",
    Environment: []string{"MY_APP=1"},
})
if err != nil {
    return err
}

watcher, err := session.WatchOutput(ghostline.WatchOptions{
    OnOutput: func(data []byte) {
        _, _ = os.Stdout.Write(data)
    },
})
if err != nil {
    return err
}
defer watcher.Close()

_ = session.Input(ctx, []byte("go test ./...\r"))
_ = session.Resize(ctx, ghostline.Size{Columns: 100, Rows: 30})

WatchOutput starts at a raw spool offset. Its callback receives a borrowed slice that is valid only until the callback returns. Copy it if it must be retained.

For a lossless client reattach or window switch, pause its watcher and use an atomic checkpoint:

watcher.Pause()
checkpoint, err := session.Checkpoint(ctx)
if err == nil {
    err = watcher.SkipTo(checkpoint.Offset)
}
if err == nil {
    _, err = client.Write(checkpoint.Replay)
}
watcher.Resume()

Bytes below Checkpoint.Offset are represented by Checkpoint.Replay; bytes written afterwards remain available to the resumed watcher.

minimux Example

examples/minimux is a small in-process terminal multiplexer built entirely on the public Manager and Session APIs:

go run ./examples/minimux
go run ./examples/minimux -- htop

It demonstrates multiple live windows, background TUI query responses, terminal resizing, output subscriptions, and atomic VT replay on window switches.

Key Action
Ctrl-B c Create a shell window
Ctrl-B n Switch to the next window
Ctrl-B p Switch to the previous window
Ctrl-B x Close the current window
Ctrl-B q Quit and terminate all windows
Ctrl-B Ctrl-B Send a literal Ctrl-B

minimux is intentionally process-local and ephemeral; it is an API example, not a replacement for tmux's persistent server and client protocol.

libghostty-vt

The repository includes the Ghostty C headers and a prebuilt macOS arm64 library. Other platforms must build libghostty-vt from Ghostty source with Zig 0.15.2:

brew install zig@0.15
git clone https://github.com/ghostty-org/ghostty
cd ghostty
/opt/homebrew/opt/zig@0.15/bin/zig build \
  -Doptimize=ReleaseFast -Demit-lib-vt=true

Point the external linker and runtime loader at that build on platforms that do not use the bundled macOS arm64 library:

export GHOSTTY_VT_DIR="$HOME/Workspace/gh/ghostty/zig-out"
export CGO_LDFLAGS="-L$GHOSTTY_VT_DIR/lib"
export DYLD_LIBRARY_PATH="$GHOSTTY_VT_DIR/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" # macOS
# export LD_LIBRARY_PATH="$GHOSTTY_VT_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"   # Linux

Builds with CGO_ENABLED=0 can import and compile ghostline, but Manager.Check and session creation return ErrUnavailable because VT emulation requires libghostty-vt.

Lifecycle

  • Call Manager.Close to terminate all sessions and release native VT state.
  • Call Session.Close to terminate one session. It is idempotent per handle.
  • Canceling Session.Wait stops waiting; it does not terminate the child.
  • Close every SpoolWatcher created by WatchOutput.
  • Spools and metadata remain after session close for recovery or diagnostics. The compatibility API exposes RemoveSpool when the application is ready to delete them.
  • Use errors.Is with ErrClosed, ErrSessionExists, ErrSessionNotFound, ErrSessionClosed, ErrInvalidSessionName, and ErrUnavailable.

The legacy NewPTY and name-based methods remain available for existing runtime adapters.

Layout

  • session.go - high-level manager, session, subscription, and checkpoint API
  • pty.go - PTY process lifecycle and compatibility API
  • ghosttyvt.go - CGo wrapper around libghostty-vt
  • spool.go - append-only output spool watcher
  • query.go - detached-mode terminal query responder
  • examples/minimux - runnable terminal multiplexer example

Documentation

Overview

Package ghostline provides embeddable pseudo-terminal sessions backed by libghostty-vt screen replays and append-only output spools.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnavailable indicates that libghostty-vt cannot be used by this build.
	ErrUnavailable = errors.New("ghostline is unavailable")
	// ErrClosed indicates that an operation requires an open hub.
	ErrClosed = errors.New("ghostline hub is closed")
	// ErrSessionExists indicates that a hub already owns the requested name.
	ErrSessionExists = errors.New("ghostline session already exists")
	// ErrSessionNotFound indicates that a hub does not own the requested name.
	ErrSessionNotFound = errors.New("ghostline session not found")
	// ErrSessionClosed indicates that a Session handle no longer refers to a
	// session owned by its hub.
	ErrSessionClosed = errors.New("ghostline session is closed")
	// ErrInvalidSessionName indicates that a name is empty or unsafe for use as
	// a spool filename.
	ErrInvalidSessionName = errors.New("invalid ghostline session name")
)

Functions

func Ping added in v0.2.0

func Ping(socketPath string) bool

Ping reports whether a ghostline server is accepting connections on the socket. It is also used by the client bootstrap to wait for a server.

Types

type Checkpoint added in v0.2.0

type Checkpoint struct {
	// Replay is a full VT replay of the visible grid and scrollback.
	Replay []byte
	// Offset is the raw spool byte position covered by Replay.
	Offset int64
}

Checkpoint is an atomic screen replay and raw output position. Bytes below Offset are represented by Replay; a paused watcher can SkipTo Offset before resuming without losing or duplicating output produced around the snapshot.

type Client added in v0.2.0

type Client struct {
	Socket string
}

Client proxies Hub operations to a ghostline Server over a Unix socket. Sessions returned by Start are remote handles with the same API as local ones, so an embedding process can restart and reconnect without ending any session.

func NewClient added in v0.2.0

func NewClient(socketPath string) *Client

func (*Client) ArchiveSpool added in v0.2.0

func (c *Client) ArchiveSpool(ctx context.Context, name string) error

func (*Client) Capture added in v0.2.0

func (c *Client) Capture(ctx context.Context, name string) ([]byte, error)

func (*Client) Check added in v0.2.0

func (c *Client) Check(ctx context.Context) error

Check reports whether the server socket is reachable.

func (*Client) Checkpoint added in v0.2.0

func (c *Client) Checkpoint(ctx context.Context, name string) (Checkpoint, error)

func (*Client) Create added in v0.2.0

func (c *Client) Create(ctx context.Context, name, directory, command string) error

func (*Client) EnsurePipe added in v0.2.0

func (c *Client) EnsurePipe(ctx context.Context, name string) error

func (*Client) Exists added in v0.2.0

func (c *Client) Exists(ctx context.Context, name string) bool

func (*Client) Input added in v0.2.0

func (c *Client) Input(ctx context.Context, name string, data []byte) error

func (*Client) Kill added in v0.2.0

func (c *Client) Kill(ctx context.Context, name string) error

func (*Client) List added in v0.2.0

func (c *Client) List(ctx context.Context) (map[string]bool, error)

func (*Client) ListCreated added in v0.2.0

func (c *Client) ListCreated(ctx context.Context) (map[string]time.Time, error)

func (*Client) Recover added in v0.2.0

func (c *Client) Recover(ctx context.Context, name string, offset, end int64) ([]byte, error)

func (*Client) RemoveSpool added in v0.2.0

func (c *Client) RemoveSpool(name string)

func (*Client) Resize added in v0.2.0

func (c *Client) Resize(ctx context.Context, name string, columns, rows int) error

func (*Client) SpoolPath added in v0.2.0

func (c *Client) SpoolPath(name string) string

func (*Client) SpoolSize added in v0.2.0

func (c *Client) SpoolSize(ctx context.Context, name string) (int64, error)

func (*Client) Start added in v0.2.0

func (c *Client) Start(ctx context.Context, options SessionOptions) (*Session, error)

Start creates a session on the server and returns its remote handle.

func (*Client) TruncateSpool added in v0.2.0

func (c *Client) TruncateSpool(ctx context.Context, name string) error

func (*Client) WaitReady added in v0.2.0

func (c *Client) WaitReady(ctx context.Context, timeout time.Duration) error

WaitReady polls the server socket until it accepts connections or the context is done.

type Hub added in v0.2.0

type Hub struct {
	// OutputDir is the directory used for spool and metadata files.
	//
	// Deprecated: configure Options.OutputDir when constructing the hub.
	OutputDir string
	// contains filtered or unexported fields
}

Hub owns a set of pseudo-terminal sessions. Sessions keep running while clients disconnect, but they remain children of the embedding process and therefore do not survive that process exiting.

func New added in v0.2.0

func New(options Options) (*Hub, error)

New constructs a session hub.

func (*Hub) ArchiveSpool added in v0.2.0

func (p *Hub) ArchiveSpool(_ context.Context, runtimeName string) error

ArchiveSpool compresses the current spool to a timestamped .gz file and prunes old archives. Best-effort diagnostics; truncation must not depend on archive success.

func (*Hub) Capture added in v0.2.0

func (p *Hub) Capture(_ context.Context, runtimeName string) ([]byte, error)

Capture renders the current emulated screen (visible grid + scrollback) with SGR styles preserved, so the client can replay a complete snapshot at its own size. This replaces the raw spool replay, which could not restore the screen when the PTY history was produced at a different size.

func (*Hub) Check added in v0.2.0

func (p *Hub) Check(ctx context.Context) error

Check reports whether the hub can construct a libghostty-vt terminal.

func (*Hub) Close added in v0.2.0

func (p *Hub) Close() error

Close terminates every managed session and releases its resources. A closed runtime cannot create new sessions.

func (*Hub) Create added in v0.2.0

func (p *Hub) Create(ctx context.Context, runtimeName, directory, command string) error

Create starts a session through the legacy name-based API.

func (*Hub) CreatedPath added in v0.2.0

func (p *Hub) CreatedPath(runtimeName string) string

CreatedPath returns the persisted creation metadata path.

func (*Hub) EnsurePipe added in v0.2.0

func (p *Hub) EnsurePipe(_ context.Context, runtimeName string) error

EnsurePipe verifies that a session exists. It is retained for compatibility with adapters that install output pipes lazily; Hub owns its spool from session creation and therefore needs no additional setup.

func (*Hub) Exists added in v0.2.0

func (p *Hub) Exists(_ context.Context, runtimeName string) bool

Exists reports whether a named session is currently running.

func (*Hub) Input added in v0.2.0

func (p *Hub) Input(_ context.Context, runtimeName string, data []byte) error

Input writes bytes to a named session's PTY verbatim.

func (*Hub) Kill added in v0.2.0

func (p *Hub) Kill(_ context.Context, runtimeName string) error

Kill terminates a named session. If the current hub does not own it, Kill uses persisted PID metadata to reclaim a process from an earlier run.

func (*Hub) List added in v0.2.0

func (p *Hub) List(context.Context) (map[string]bool, error)

List returns the names of all currently running sessions.

func (*Hub) ListCreated added in v0.2.0

func (p *Hub) ListCreated(context.Context) (map[string]time.Time, error)

ListCreated returns persisted session creation times. A restarted embedding process can use them to identify and reclaim children it no longer owns.

func (*Hub) PIDPath added in v0.2.0

func (p *Hub) PIDPath(runtimeName string) string

PIDPath returns the persisted child process ID metadata path.

func (*Hub) Recover added in v0.2.0

func (p *Hub) Recover(_ context.Context, runtimeName string, offset, end int64) ([]byte, error)

Recover returns the spool bytes in [offset, end), the raw PTY output a client still needs after its anchor. Callers can prefer this over a full snapshot whenever the spool still covers the anchor, so switching back to a retained surface renders the missing tail without clearing the screen.

func (*Hub) RemoveSpool added in v0.2.0

func (p *Hub) RemoveSpool(runtimeName string)

RemoveSpool removes a session's spool, metadata, and archives. Callers must terminate the session and close its watchers first.

func (*Hub) Resize added in v0.2.0

func (p *Hub) Resize(_ context.Context, runtimeName string, columns, rows int) error

Resize updates a named session's PTY and VT grid.

func (*Hub) Session added in v0.2.0

func (p *Hub) Session(name string) (*Session, bool)

Session returns a handle for a managed session name, including sessions whose process has already exited but has not been closed or removed.

func (*Hub) Sessions added in v0.2.0

func (p *Hub) Sessions() []*Session

Sessions returns all managed sessions ordered by creation time and name, including sessions whose process has already exited.

func (*Hub) SpoolPath added in v0.2.0

func (p *Hub) SpoolPath(runtimeName string) string

SpoolPath returns the raw output spool path, or an empty string for an invalid session name.

func (*Hub) SpoolSize added in v0.2.0

func (p *Hub) SpoolSize(_ context.Context, runtimeName string) (int64, error)

SpoolSize returns the number of raw output bytes currently persisted.

func (*Hub) Start added in v0.2.0

func (p *Hub) Start(ctx context.Context, options SessionOptions) (*Session, error)

Start creates and starts a session.

func (*Hub) TruncateSpool added in v0.2.0

func (p *Hub) TruncateSpool(_ context.Context, runtimeName string) error

TruncateSpool compacts the live spool in place. The copyOutput goroutine keeps its O_APPEND file descriptor, so output continues into the same inode from byte zero. Consumers should reset their offsets and reanchor.

type Options added in v0.2.0

type Options struct {
	// OutputDir stores append-only output spools and session metadata. An empty
	// value uses $HOME/.ghostline/output.
	OutputDir string
	// DefaultSize is used when SessionOptions.Size is zero. The default is
	// 120 columns by 36 rows.
	DefaultSize Size
}

Options configures a Hub. Zero values select documented defaults.

type PTY

type PTY = Hub

PTY is the compatibility name for Hub. Deprecated: use Hub and New.

func NewPTY

func NewPTY(outputDir string) *PTY

NewPTY constructs a hub with the legacy constructor. Deprecated: use New with Options.

type QueryResponder

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

QueryResponder answers terminal capability queries while a session has no attached terminal client. TUIs such as Codex send DA/DSR/OSC/kitty keyboard queries at startup. A raw PTY has nobody to answer until a client attaches, so the application may downgrade itself (for example disabling colors). Replies are written back into the PTY as input, never into output.

func NewQueryResponder

func NewQueryResponder() *QueryResponder

NewQueryResponder returns a responder initialized to a 120x36 terminal.

func (*QueryResponder) Feed

func (r *QueryResponder) Feed(data []byte) [][]byte

Feed scans output bytes for complete terminal queries and returns the replies to write back into the PTY. Queries split across chunks are buffered until complete or until they prove not to be queries.

func (*QueryResponder) Resize

func (r *QueryResponder) Resize(columns, rows int)

Resize updates the window size reported in XTWINOPS replies.

type Server added in v0.2.0

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

Server owns PTY sessions in a standalone process so clients (for example a headless daemon) can restart without ending any session. The server writes raw PTY bytes to the same append-only spool files; clients read those files directly for incremental output and recovery.

The wire protocol is one JSON object per line on a Unix socket. Binary payloads (input, snapshots) are base64 fields.

func NewServer added in v0.2.0

func NewServer(options Options) (*Server, error)

func (*Server) Close added in v0.2.0

func (s *Server) Close() error

Close stops accepting connections. In-flight handlers finish before the process exits.

func (*Server) Serve added in v0.2.0

func (s *Server) Serve(socketPath string) error

Serve listens on socketPath and handles requests until the listener closes. The socket directory must exist and be private.

type Session added in v0.2.0

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

Session is a stable handle to one pseudo-terminal session, local or remote.

func (*Session) Checkpoint added in v0.2.0

func (s *Session) Checkpoint(ctx context.Context) (Checkpoint, error)

Checkpoint captures a replay and its exact spool boundary atomically.

func (*Session) Close added in v0.2.0

func (s *Session) Close() error

Close terminates the session. It is idempotent for this handle.

func (*Session) Name added in v0.2.0

func (s *Session) Name() string

Name returns the session's unique name.

func (*Session) SpoolPath added in v0.2.0

func (s *Session) SpoolPath() string

SpoolPath returns the append-only raw output spool path.

func (*Session) SpoolSize added in v0.2.0

func (s *Session) SpoolSize(ctx context.Context) (int64, error)

SpoolSize returns the current raw output spool size.

func (*Session) WatchOutput added in v0.2.0

func (s *Session) WatchOutput(options WatchOptions) (*SpoolWatcher, error)

WatchOutput subscribes to raw output and starts the watcher before returning.

type SessionOptions added in v0.2.0

type SessionOptions struct {
	// Name identifies the session and its spool files. It must be a single,
	// non-empty path component.
	Name string
	// Directory is the child's working directory. An empty value inherits the
	// embedding process's working directory.
	Directory string
	// Command is evaluated by "sh -lc". An empty value starts $SHELL, falling
	// back to sh.
	Command string
	// Size is the initial terminal grid size. A zero value uses the hub's
	// default size.
	Size Size
	// Environment entries use KEY=value form and override inherited values.
	// TERM and COLORTERM default to xterm-256color and truecolor respectively.
	Environment []string
}

SessionOptions configures one pseudo-terminal session.

type Size added in v0.2.0

type Size struct {
	// Columns is the number of character cells per line.
	Columns int
	// Rows is the number of lines in the grid.
	Rows int
}

Size is a terminal grid size in cells.

type SpoolRecoverer

type SpoolRecoverer interface {
	Recover(context.Context, string, int64, int64) ([]byte, error)
}

SpoolRecoverer reads a contiguous byte range from a session's append-only spool. Hub implements it so a consumer can recover an evicted client anchor without forcing a full screen reset and replay.

type SpoolWatcher

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

SpoolWatcher reads an append-only spool from a persisted byte offset, draining to EOF whenever the file grows. The byte slice passed to onBytes is valid only for the duration of the callback; callers must copy it to retain it.

The watcher also detects in-place truncation (spool compaction). After a truncate the file size drops below the watcher offset; the watcher re-bases to offset zero and calls onRotate so the consumer can invalidate old offsets instead of silently skipping bytes.

func NewSpoolWatcher

func NewSpoolWatcher(path string, offset int64, onBytes func([]byte), onRotate func(), onOverflow func()) (*SpoolWatcher, error)

NewSpoolWatcher returns a watcher positioned at offset in the file at path. The callbacks may be nil. Start begins polling.

func (*SpoolWatcher) Close

func (w *SpoolWatcher) Close()

Close stops the watcher and releases its file descriptor. It is safe to call multiple times.

func (*SpoolWatcher) Offset

func (w *SpoolWatcher) Offset() int64

Offset returns the next byte position the watcher will deliver.

func (*SpoolWatcher) Pause

func (w *SpoolWatcher) Pause()

Pause blocks until any in-flight drain finishes, then prevents new drains. Use it while preparing a checkpoint replay so live reads cannot interleave.

func (*SpoolWatcher) Ping

func (w *SpoolWatcher) Ping()

Ping asks the watcher to check for output without waiting for its next poll.

func (*SpoolWatcher) Resume

func (w *SpoolWatcher) Resume()

Resume re-enables draining after Pause and asks the watcher to check immediately.

func (*SpoolWatcher) SetMaxBytes

func (w *SpoolWatcher) SetMaxBytes(maxBytes int64)

SetMaxBytes configures the spool size cap before Start. When the watcher passes the cap it calls onOverflow so the consumer can compact the spool.

func (*SpoolWatcher) SkipTo

func (w *SpoolWatcher) SkipTo(offset int64) error

SkipTo re-bases the watcher to a byte position covered by a snapshot. It must be called while paused and the offset must be within the current file; any unread bytes below the target were already rendered by the snapshot and must not be delivered again.

func (*SpoolWatcher) Start

func (w *SpoolWatcher) Start()

Start begins watching. Repeated calls are safe and have no effect.

type VTTerminal

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

VTTerminal is a libghostty-vt terminal emulator that renders raw PTY bytes into a complete screen snapshot (visible grid + scrollback) with SGR styles preserved. It is the server-side counterpart of the Ghostty client, so a replayed snapshot matches exactly what the client would have rendered.

func NewVTTerminal

func NewVTTerminal(cols, rows int) (*VTTerminal, error)

NewVTTerminal creates a terminal emulator with the given grid size.

func (*VTTerminal) Close

func (v *VTTerminal) Close()

Close releases the native terminal state. It must be called at most once.

func (*VTTerminal) Feed

func (v *VTTerminal) Feed(data []byte)

Feed parses raw PTY bytes into the emulated terminal state.

func (*VTTerminal) Resize

func (v *VTTerminal) Resize(cols, rows int)

Resize reflows the emulated terminal. The caller keeps the real PTY size in sync so snapshots are rendered at the client's dimensions.

func (*VTTerminal) Snapshot

func (v *VTTerminal) Snapshot() ([]byte, error)

Snapshot renders the current emulated screen (visible grid + scrollback) as VT sequences that preserve colors and styles.

type WatchOptions added in v0.2.0

type WatchOptions struct {
	// Offset is the first spool byte to deliver.
	Offset int64
	// MaxBytes invokes OnOverflow after the watcher passes the limit. Zero uses
	// the watcher default.
	MaxBytes int64
	// OnOutput receives borrowed output bytes. Copy the slice to retain it after
	// the callback returns.
	OnOutput func([]byte)
	// OnTruncate runs when the spool is compacted in place.
	OnTruncate func()
	// OnOverflow runs when Offset passes MaxBytes.
	OnOverflow func()
}

WatchOptions configures an output subscription.

Directories

Path Synopsis
cmd
ghostline command
examples
minimux command
Command minimux is a tiny in-process terminal multiplexer built on ghostline.
Command minimux is a tiny in-process terminal multiplexer built on ghostline.

Jump to

Keyboard shortcuts

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