ghostline

package module
v0.1.0 Latest Latest
Warning

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

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

README

ghostline

Server-side PTY sessions with a Ghostty VT emulator.

ghostline is the terminal runtime layer behind Warren: it owns one pseudo-terminal per session, streams raw PTY bytes to an append-only spool, and renders screen snapshots with libghostty-vt — the same terminal core the Ghostty client uses. Snapshots therefore preserve colors, alt-screen, DEC 2026, and scrollback exactly as the client would render them, at any requested size.

It is designed to be embedded (a tmux-free runtime for headless daemons) and usable on its own (create sessions, feed input, render snapshots, kill).

Requirements

  • Go 1.25+
  • libghostty-vt built from the 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

The CGo wrapper expects include and lib under GHOSTTY_VT_DIR (default $HOME/Workspace/gh/ghostty/zig-out).

Usage

import "github.com/abcdlsj/ghostline"

manager := ghostline.NewPTY(outputDir)
err := manager.Create(ctx, "ghost_abc", "/path/to/worktree", "codex")
manager.Input(ctx, "ghost_abc", []byte("hello\r"))
snapshot, err := manager.Capture(ctx, "ghost_abc")
manager.Resize(ctx, "ghost_abc", 100, 30)
manager.Kill(ctx, "ghost_abc")

Layout

  • pty.go — PTY session lifecycle and spool management
  • ghosttyvt.go — CGo wrapper around libghostty-vt (VTTerminal)
  • spool.go — append-only output spool watcher
  • query.go — detached-mode terminal query responder (DA/DSR/OSC/kitty)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type PTY

type PTY struct {
	OutputDir string
	// contains filtered or unexported fields
}

PTY is a tmux-free runtime that owns one pseudo-terminal per session. The daemon keeps the child alive after every client disconnects, and raw PTY bytes are appended to the same per-session spool consumed by SpoolWatcher, so the output pipeline (ring, recovery anchors, reanchor) is unchanged.

Known differences from the tmux adapter:

  • Input bytes are written to the PTY verbatim, so there is no tmux paste-vs-key translation layer and kitty-protocol keys (for example Shift+Enter) reach the application unchanged.
  • A daemon restart closes the PTY master and ends its sessions. tmux sessions survive a daemon restart because tmux owns them; the PTY runtime owns children directly and does not implement adoption yet.

func NewPTY

func NewPTY(outputDir string) *PTY

func (*PTY) ArchiveSpool

func (p *PTY) 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. Mirrors the tmux adapter's spool helpers.

func (*PTY) Capture

func (p *PTY) 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 (*PTY) Check

func (p *PTY) Check(context.Context) error

Check reports whether the runtime can start. Unlike tmux there is no external binary to find, so the check always succeeds.

func (*PTY) Create

func (p *PTY) Create(_ context.Context, runtimeName, directory, command string) error

func (*PTY) CreatedPath

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

func (*PTY) EnsurePipe

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

EnsurePipe is idempotent by construction: the PTY runtime owns the spool from Create on, so there is no pipe to install on adopt.

func (*PTY) Exists

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

func (*PTY) Input

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

func (*PTY) Kill

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

func (*PTY) List

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

func (*PTY) ListCreated

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

ListCreated returns live sessions with their creation time, persisted in metadata files so a restarted daemon can reclaim orphans it no longer has a process handle for.

func (*PTY) PIDPath

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

func (*PTY) Recover

func (p *PTY) 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. Host prefers this over a full reanchor snapshot whenever the spool still covers the anchor, so switching back to a retained surface renders the missing tail without clearing the screen.

func (*PTY) RemoveSpool

func (p *PTY) RemoveSpool(runtimeName string)

func (*PTY) Resize

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

func (*PTY) SpoolPath

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

func (*PTY) SpoolSize

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

func (*PTY) TruncateSpool

func (p *PTY) 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; Host bumps the epoch and reanchors clients.

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; tmux answers them through its own emulator, but a raw PTY has nobody to answer until a client attaches, so the application would downgrade itself (for example disabling colors). Replies are written back into the PTY as input, never into the output spool.

func NewQueryResponder

func NewQueryResponder() *QueryResponder

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)

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. PTY implements it so Host can recover from the spool even after the in-memory ring evicted the client's anchor, avoiding 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. It is the Go counterpart of the Swift OutputSpoolWatcher: capture-pane is no longer used for live output.

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 Host can bump the epoch and reanchor every client instead of silently skipping bytes.

func NewSpoolWatcher

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

func (*SpoolWatcher) Close

func (w *SpoolWatcher) Close()

func (*SpoolWatcher) Offset

func (w *SpoolWatcher) Offset() int64

func (*SpoolWatcher) Pause

func (w *SpoolWatcher) Pause()

Pause blocks until any in-flight drain finishes, then prevents new drains. Host uses it while preparing a reanchor so the snapshot replay can never race with live reads.

func (*SpoolWatcher) Ping

func (w *SpoolWatcher) Ping()

Ping nudges the watcher after input, matching the old outputWake behavior without making it the delivery mechanism.

func (*SpoolWatcher) Resume

func (w *SpoolWatcher) Resume()

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; Host archives, truncates, bumps the epoch, and reanchors clients.

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; 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()

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)

func (*VTTerminal) Close

func (v *VTTerminal) Close()

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.

Jump to

Keyboard shortcuts

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