termemu

package module
v0.0.0-...-4007576 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 26 Imported by: 0

README

termemu

A small Go library for emulating a terminal and rendering output through a pluggable frontend. It runs commands inside a PTY (or a fallback pipe backend) and exposes screen state, colors, and view flags.

Features

  • PTY-backed command execution with screen emulation
  • Pluggable Frontend interface for rendering or collecting output
  • Reusable PTY backend — run multiple commands through the same PTY
  • Scrollback buffer with stable row IDs and reflow-on-read
  • Screen accessors for plain text, ANSI output, and styled lines
  • Color value type with 256-color, bright, and RGB support; truecolor is advertised to children via COLORTERM=truecolor (DA1 has no truecolor parameter)
  • Minimal backend abstraction for PTY or no-PTY environments
  • Rune-per-cell or grapheme-cluster text tokenization
  • Mouse reporting (X10/UTF-8/SGR/urxvt encodings)
  • Kitty keyboard protocol mode parsing and key encoding support
  • Synchronized output (DECSET/DECRST 2026) exposed as VFSynchronizedOutput
  • OSC 8 hyperlinks tracked per span (Span.Link)
  • OSC 52 clipboard write path via the optional ClipboardSink interface
  • OSC 6/7 working-directory announcements mapped to VSCurrentDirectory
  • 256-color palette and default fg/bg/cursor colors as terminal state (OSC 4/10/11/12, resettable via 104/110–112, queryable by the child), resolved by frontends through LockedView.PaletteColor etc.
  • Shell integration: OSC 133 prompt marks (ShellIntegration) and OSC 9/777 notifications (NotificationSink)
  • Screen snapshots (Snapshot/Restore) for saving and replaying terminal state

Requirements

  • Go 1.24+

Install

go get github.com/ricochet1k/termemu

Usage

Run a command and print the rendered screen
package main

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/ricochet1k/termemu"
)

func main() {
	// Use the EmptyFrontend if you don't need UI callbacks.
	backend := &termemu.PTYBackend{}
	cmd := exec.Command("sh", "-c", "printf 'Hello World'")
	if err := backend.Attach(cmd); err != nil {
		fmt.Fprintln(os.Stderr, "Attach error:", err)
		return
	}
	if err := cmd.Start(); err != nil {
		fmt.Fprintln(os.Stderr, "cmd.Start error:", err)
		return
	}

	term := termemu.NewWithMode(&termemu.EmptyFrontend{}, backend, termemu.TextReadModeRune)
	if term == nil {
		fmt.Println("failed to create terminal")
		return
	}

	_ = cmd.Wait()

	// Dump the screen contents to stdout.
	termemu.PrintTerminal(term)
}
Capture redraws with a custom frontend
package main

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/ricochet1k/termemu"
)

type loggingFrontend struct{}

func (loggingFrontend) Bell(v termemu.LockedView) {}
func (loggingFrontend) RegionChanged(v termemu.LockedView, r termemu.Region, _ termemu.ChangeReason) {
	fmt.Printf("region changed: (%d,%d)-(%d,%d)\n", r.X, r.Y, r.X2, r.Y2)
}
func (loggingFrontend) ScrollLines(v termemu.LockedView, delta int) {}
func (loggingFrontend) CursorMoved(v termemu.LockedView, x, y int) {}
func (loggingFrontend) StyleChanged(v termemu.LockedView, s termemu.Style) {}
func (loggingFrontend) ViewFlagChanged(v termemu.LockedView, flag termemu.ViewFlag, value bool) {}
func (loggingFrontend) ViewIntChanged(v termemu.LockedView, flag termemu.ViewInt, value int) {}
func (loggingFrontend) ViewStringChanged(v termemu.LockedView, flag termemu.ViewString, value string) {}

func main() {
	backend := &termemu.PTYBackend{}
	cmd := exec.Command("sh", "-c", "printf '\\e[31mred\\e[0m' && echo")
	if err := backend.Attach(cmd); err != nil {
		fmt.Fprintln(os.Stderr, "Attach error:", err)
		return
	}
	if err := cmd.Start(); err != nil {
		fmt.Fprintln(os.Stderr, "cmd.Start error:", err)
		return
	}
	term := termemu.NewWithMode(loggingFrontend{}, backend, termemu.TextReadModeRune)
	_ = cmd.Wait()

	for y := 0; y < 1; y++ {
		fmt.Println(term.ANSILine(y))
	}
}
Interactive terminal with TTY frontend
package main

import (
	"os"
	"os/exec"
	"os/signal"
	"syscall"

	"github.com/creack/pty"
	"github.com/creack/termios/raw"
	"github.com/ricochet1k/termemu"
)

func main() {
	tios, err := raw.MakeRaw(os.Stdin.Fd())
	if err != nil {
		panic(err)
	}
	defer raw.TcSetAttr(os.Stdin.Fd(), tios)

	tty := termemu.NewTTYFrontend(os.Stdout)
	cmd := exec.Command("sh")
	backend := &termemu.PTYBackend{}
	backend.Attach(cmd)
	cmd.Start()
	term := termemu.NewWithMode(tty, backend, termemu.TextReadModeRune)

	resize := func() {
		ws, _ := pty.GetsizeFull(os.Stdout)
		w, h := int(ws.Cols), int(ws.Rows)
		term.Resize(w, h)
		tty.Attach(termemu.Region{X: 0, Y: 0, X2: w, Y2: h})
	}
	resize()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGWINCH)

	inputCh := make(chan []byte, 8)
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := os.Stdin.Read(buf)
			if n > 0 {
				chunk := make([]byte, n)
				copy(chunk, buf[:n])
				inputCh <- chunk
			}
			if err != nil {
				return
			}
		}
	}()

	doneCh := make(chan error, 1)
	go func() { doneCh <- cmd.Wait() }()

	for {
		select {
		case b := <-inputCh:
			if len(b) > 0 {
				term.Write(b)
			}
		case <-sigCh:
			resize()
		case <-doneCh:
			tty.Detach()
			return
		}
	}
}
Reusable PTY (multiple commands)
backend := &termemu.PTYBackend{}

cmd1 := exec.Command("sh", "-c", "echo first")
backend.Attach(cmd1)
cmd1.Start()
cmd1.Wait()

// Same PTY, same terminal state:
cmd2 := exec.Command("sh", "-c", "echo second")
backend.Attach(cmd2)
cmd2.Start()
Adopting an existing pty

If something else already opened the pty pair, hand it to the backend with Adopt (or AdoptMaster). The backend takes ownership of the files it is given: Close (also via Terminal.Close) closes them. Call Adopt before constructing the Terminal or opening the backend.

master, slave := existingFiles() // both are *os.File
backend := &termemu.PTYBackend{}
if err := backend.Adopt(master, slave); err != nil {
    // nil master, closed/already-open backend, or already has a pty
}
term := termemu.New(&termemu.EmptyFrontend{}, backend)

// Raw file descriptors can be wrapped as files first:
slave := os.NewFile(uintptr(fd), "pts")
_ = backend.Adopt(master, slave)
// Master only (no Attach; Read surfaces EOF/EIO instead of reuse):
_ = backend.AdoptMaster(master)

With slave == nil (from AdoptMaster), Open and Attach return an error because there is no slave to attach a command to. Read then does a plain master read and surfaces EOF/EIO so the read loop can exit; it does not wait for reuse. Write, SetSize, Close, PID, and Wait behave as usual (PID is 0 and Wait errors since no command was attached).

Scrollback
sb := term.Scrollback()
base, next := sb.Range()
for id := base; id < next; id++ {
    row, ok := sb.Row(id)
    if ok {
        fmt.Println(row.PlainTextString())
    }
}

// Reflown at current terminal width:
logical := sb.LogicalLine(ScrollID(3))
for _, row := range logical {
    fmt.Println(row.PlainTextString())
}

// Row counts and clearing (CSI 3 J also clears; screen untouched):
fmt.Println(sb.Len())
sb.Clear()

// Scrollback has its own snapshot/restore, separate from Terminal.Snapshot:
snap := sb.Snapshot()
if err := sb.Restore(snap); err != nil {
    // errors.Is(err, termemu.ErrScrollbackSnapshotInvalid)
}

Only scrolls starting at the top of the screen enter scrollback; scrolling inside a mid-screen scroll region (CSI r margins) never does. Returned rows own copies of their spans, so mutating them is safe. ScrollIDs are monotonic across Clear (no ID is ever reused). Do not call Terminal.Scrollback methods from inside a Frontend callback (deadlock: callbacks already hold the terminal lock). Inside a callback use v.Scrollback(), which returns the no-lock LockedScrollback (Range, Row, LogicalLine, MovePos, Len). From a callback, use v.Snapshot() and v.Scrollback().Snapshot() for deep copies; Terminal.Snapshot and *Scrollback.Snapshot lock and would deadlock there.

To avoid polling, implement the optional termemu.ScrollbackListener extension on your frontend. It is called once per capture batch, with the terminal lock held, and receives the newly appended [first, next) IDs:

type myFrontend struct{ *termemu.EmptyFrontend }

func (f *myFrontend) ScrollbackRowsAppended(v termemu.LockedView, first, next termemu.ScrollID) {
    sb := v.Scrollback() // callback already holds the lock
    for id := first; id < next; id++ {
        if row, ok := sb.Row(id); ok {
            fmt.Println(row.PlainTextString())
        }
    }
}

Alternate-screen output never produces rows and Clear() does not call the listener. Do not retain the view beyond the callback.

Lifecycle

Terminals own a background read-loop goroutine. Shut them down explicitly:

term := termemu.NewWithMode(&termemu.EmptyFrontend{}, backend, termemu.TextReadModeRune)
// ... run commands ...
_ = term.Close()  // idempotent; unblocks Read so the loop exits promptly
<-term.Done()     // closed when the read loop exits
if err := term.Err(); err != nil {
    // non-nil: the loop died on a backend error (io.EOF is clean, Err() == nil)
}

Close, Done, and Err do not take the terminal state lock, so they can be called from a Frontend callback (which already holds it) without deadlocking.

PTYBackend.Close() (also called by Terminal.Close()) closes the pty pair; later Read/Write/Attach fail with io.ErrClosedPipe instead of reopening it. Resize(w, h) emits one full-screen RegionChanged with the CRResize reason so frontends know to repaint at the new size. Shrinking the height on the main screen pushes the lost top rows into scrollback and shifts the surviving content up; resizing the alt screen only resizes it.

PTYBackend.Attach() injects TERM=xterm-256color and COLORTERM=truecolor into the child environment by default. Truecolor is advertised via COLORTERM because DA1 has no truecolor parameter. Set PTYBackend.AutoEnv to override the list, or to an empty non-nil slice to disable injection:

backend := &termemu.PTYBackend{AutoEnv: []string{"TERM=screen-256color"}}
backend := &termemu.PTYBackend{AutoEnv: []string{}} // inject nothing

Injected entries replace same-key values already present in cmd.Env; other environment variables are left alone.

Locking: plain read methods (Size, CursorPos, Line, ANSILine, StyledLine, StyledLines, GetViewFlag/Int/String) take the terminal lock themselves, so they are safe from any goroutine. For atomic multi-reads use term.View(func(v termemu.LockedView) { ... }). Every Frontend callback also receives a LockedView as its first argument; read state through it (including v.Scrollback()) instead of the locking methods, which would deadlock from a callback.

term.View(func(v termemu.LockedView) {
    w, h := v.Size()
    line := v.Line(0)
    _, _ = w, h, line
})
Pause and process handoff

Terminal.Pause(ctx) parks the read loop and returns a Handoff containing screen and scrollback snapshots, the terminal size, and the reader/parser state (bytes already read but not applied, plus any partial escape command). Terminal.Resume() continues a same-process paused loop.

To move a terminal to another process, the old process pauses, sends the backend file descriptors, then closes/exits. The new process adopts the fds and creates a terminal with NewFromHandoff, which restores everything before starting the read loop:

// Old process: pause, then send h and the fds (SCM_RIGHTS, cmd.ExtraFiles,
// or your IPC of choice).
h, err := term.Pause(ctx)
master, slave := backend.Master(), backend.Slave() // owned by backend
// ... transfer master/slave and h ...
_ = term.Close() // now safe to close/exit; Resume is no longer needed

// New process: wrap received fds and adopt them, then rebuild.
backend := &termemu.PTYBackend{}
if err := backend.Adopt(os.NewFile(masterFD, "ptmx"), os.NewFile(slaveFD, "pts")); err != nil {
    // handle
}
term, err := termemu.NewFromHandoff(frontend, backend, h)

PTYBackend.Master() and Slave() return the owned files (Slave() may be nil after AdoptMaster). The sender may close its copies after transferring them, because the descriptor is duplicated into the receiving process. NewFromHandoff validates the backend and size; if the receiving terminal has a different size, set h.Size to the target size before calling it. The snapshot content is resized into that size. On a restore error it closes the backend. Use AdoptMaster when only the master is available (no Attach/reuse after that).

Pause interrupts a blocked read by setting a deadline through the optional SetReadDeadline method (PTYBackend, NoPTYBackend when its reader supports deadlines such as *os.File, and TeeBackend). A backend may instead provide Wake to interrupt the read. If neither is available (or the deadline setter returns errors.ErrUnsupported and there is no Wake), Pause returns ErrPauseUnsupported immediately rather than blocking until the next byte.

h, err := term.Pause(ctx) // leaves the loop paused
if err != nil { /* cancelled or loop already stopped */ }
if err := term.Resume(); err != nil { /* ... */ }
Snapshots

Snapshot() deep-copies both screen buffers plus cursor, style, margins, auto-wrap, wrapped flags, view flags/ints/strings, keyboard modes, and the active OSC 8 link. The scrollback buffer and backend/process state are not included. Restore() accepts snapshots of any size: it resizes the screens (and the backend) to the snapshot dimensions, truncating or padding lines, and emits a full-screen RegionChanged with CRRestore so frontends repaint.

From a Frontend callback or inside Terminal.View, use v.Snapshot() and v.Scrollback().Snapshot() instead: they are the no-lock variants of Terminal.Snapshot() and Scrollback.Snapshot(). Restore has no view variant (the view is read-only).

snap := term.Snapshot()
// ... run more output or otherwise mutate the screen ...
if err := term.Restore(snap); err != nil {
    // errors.Is(err, termemu.ErrSnapshotSizeMismatch) for an invalid snapshot
}

Restoring does not touch the scrollback buffer, and a snapshot stays independent of the terminal after it is taken. Use Scrollback.Snapshot() and Scrollback.Restore() separately when scrollback history must be saved; they preserve row IDs and keep the ring's Base/Next monotonic.

New screens start with auto-wrap (DECAWM, CSI ? 7) enabled, matching xterm.

Serialization

All public value types (Style, Span, Line, Region, Pos, Row, ScrollbackSnapshot, ScreenState, and Snapshot) round-trip through both encoding/json and encoding/gob. Style and the keyboard-mode state inside Snapshot use custom JSON/gob encoders; Style's JSON shape is {"fg":<uint32>,"bg":<uint32>,"underlineColor":<uint32>}, where the numbers are the packed internal representation (not palette indexes), not {}.

Modern terminal features
  • Synchronized output: DECSET/DECRST 2026 sets/clears the VFSynchronizedOutput view flag. The core performs no batching itself; frontends may defer repaints while the flag is set.
  • Hyperlinks: OSC 8 ; params ; URI starts a link, an empty URI ends it. Spans written while a link is active carry it in Span.Link (preserved through wrapping, scrollback, and line slicing; blank/erased spans never carry a link). The TTY frontend does not re-emit OSC 8.
  • Clipboard: if the frontend implements ClipboardSink (Clipboard(v LockedView, selection string, data []byte)), OSC 52 payloads are base64-decoded and forwarded with the requested selection. Queries (data == "?") are never answered or forwarded, and malformed base64 is ignored. Called with the terminal lock held and a LockedView: read through the view, do not call locking terminal methods from Clipboard.
  • Working directory: OSC 7 (and legacy OSC 6) set VSCurrentDirectory. OSC 7 no longer sets VSCurrentFile; no OSC sequence sets it.

API highlights

  • NewWithMode(frontend, backend, mode, opts...) — creates a terminal. Supports WithScrollback(rows).
  • New(frontend, backend) — shorthand for NewWithMode(…, TextReadModeGrapheme).
  • NewNoPTYBackend(reader, writer) — creates a backend from provided pipes.
  • PTYBackend.Attach(*exec.Cmd) — wires a command into the PTY. Reusable.
  • PTYBackend.Adopt(master, slave) / AdoptMaster(master) — adopt and own an existing pty pair (slave may be nil).
  • Terminal.Pause(ctx) / Terminal.Resume() — park/capture and continue a terminal's read loop, including buffered bytes and a partial escape command.
  • NewFromHandoff(frontend, backend, handoff, opts...) — restore a paused terminal in another process (after adopting the transferred backend fds).
  • PTYBackend.Master()/Slave()/SetReadDeadline() — access handoff fds and interrupt a blocked read for pausing.
  • Terminal.Close()/Done()/Err() — shut down the read loop and observe how it exited.
  • Terminal.Scrollback() — returns the scrollback buffer.
  • Scrollback.Snapshot()/Restore(ScrollbackSnapshot) — deep-copy and restore scrollback history (separate from terminal snapshots; callback-safe snapshot via LockedScrollback.Snapshot()).
  • Terminal.Snapshot()/Restore(Snapshot) — deep-copy and restore terminal screen state (restores at the snapshot size; scrollback excluded; callback-safe snapshot via LockedView.Snapshot()).
  • PrintTerminal(t Terminal) / PrintTerminalTo(w, t) — debug printing of screen contents.
  • NewTTYFrontend(out) — terminal frontend that writes ANSI to output.

Testing

go test ./...
Differential testing against tmux

The opt-in TestDifferentialTmux starts a private tmux server (it does not need to run inside tmux), renders a sequence in tmux, renders the same sequence through termemu, feeds termemu's screen back through tmux for canonical capture, and compares the screen and cursor. It needs tmux installed:

scripts/run_tmux_diff.sh
# or
TERMEMU_TMUX_DIFF=1 go test -run '^TestDifferentialTmux' -v .

The curated corpus compares styled screens and cursor positions; a seeded random corpus compares visible text and cursor positions. The harness normalizes both paths through tmux, including tab expansion and blank-line attributes.

License

MIT. See LICENSE.

Documentation

Index

Constants

View Source
const (
	MMNone int = iota
	MMPress
	MMPressRelease
	MMPressReleaseMove
	MMPressReleaseMoveAll
)

Mouse modes for VI_MouseMode

View Source
const (
	CursorShapeDefault         int = 0 // terminal default (blinking block)
	CursorShapeBlinkBlock      int = 1
	CursorShapeSteadyBlock     int = 2
	CursorShapeBlinkUnderline  int = 3
	CursorShapeSteadyUnderline int = 4
	CursorShapeBlinkBar        int = 5
	CursorShapeSteadyBar       int = 6
)

Cursor shapes for VICursorShape (DECSCUSR). The value is the DECSCUSR Ps parameter, so it can be forwarded to a terminal frontend unchanged.

View Source
const (
	MEX10 int = iota
	MEUTF8
	MESGR
	MEURXVT
)

Mouse encodings for VI_MouseEncoding

View Source
const (
	ColBlack   = 0
	ColRed     = 1
	ColGreen   = 2
	ColYellow  = 3
	ColBlue    = 4
	ColMagenta = 5
	ColCyan    = 6
	ColWhite   = 7
)

Public color constants for 256-color mode

View Source
const (
	ColorType256 = 0
	ColorTypeRGB = 1
)

Color constants for the Color type

View Source
const (
	// low 2 bits
	MBtn1    MouseBtn = 0
	MBtn2    MouseBtn = 1
	MBtn3    MouseBtn = 2
	MRelease MouseBtn = 3

	// flags
	MShift   MouseFlag = 4
	MMeta    MouseFlag = 8
	MControl MouseFlag = 16
	MMotion  MouseFlag = 32
	MWheel   MouseFlag = 64
)
View Source
const DefaultScrollbackRows = 1000

DefaultScrollbackRows is the default capacity of the scrollback ring buffer.

View Source
const ESC byte = 27
View Source
const Version = "0.1"

Version is the emulator version reported by XTVERSION (CSI > q).

Variables

View Source
var ErrHandoffInvalid = errors.New("termemu: invalid handoff")

ErrHandoffInvalid is returned by NewFromHandoff when the handoff is missing required state (a backend, a positive size, or matching snapshot dimensions).

View Source
var ErrPauseUnsupported = errors.New("termemu: Pause requires a backend that can interrupt a blocked read")

ErrPauseUnsupported is returned by Pause when the backend cannot interrupt a blocked read (it implements neither SetReadDeadline nor Wake). Waiting for the next byte would make Pause unbounded, so it fails instead.

View Source
var ErrScrollbackSnapshotInvalid = errors.New("termemu: invalid scrollback snapshot")

ErrScrollbackSnapshotInvalid is returned by Scrollback.Restore when the snapshot's row IDs are non-contiguous, its Next/Base range does not match the row count, or it otherwise cannot describe a valid ring buffer.

View Source
var ErrSnapshotSizeMismatch = errors.New("termemu: snapshot dimensions do not match terminal size")

ErrSnapshotSizeMismatch is returned by Terminal.Restore when the snapshot dimensions do not match the current terminal size. Restore never resizes the pty or screens implicitly.

Functions

func DebugFlags

func DebugFlags()

func PrintTerminal

func PrintTerminal(t Terminal)

PrintTerminal prints the terminal screen contents to stdout for debugging. It uses only the public Terminal interface, with proper locking.

func PrintTerminalTo

func PrintTerminalTo(w io.Writer, t Terminal)

PrintTerminalTo prints the terminal screen contents to w for debugging. It uses only the public Terminal interface, with proper locking.

Types

type Backend

type Backend interface {
	io.Reader
	io.Writer
	io.Closer
	SetSize(w, h int) error
}

Backend provides the IO connection to the terminal emulator. Implementations should be initialized before constructing a Terminal.

type BatchFrontend

type BatchFrontend interface {
	// PrepareBatch is called with the terminal lock held and receives a
	// LockedView, which is valid only for the duration of the call. It must
	// render everything accumulated since the previous frame into
	// frontend-owned memory and must not perform I/O: blocking here blocks
	// every terminal reader.
	//
	// PrepareBatch is not called concurrently with itself; the terminal lock
	// serializes the calls.
	PrepareBatch(v LockedView)

	// WriteBatch publishes the memory rendered by the most recent
	// PrepareBatch. It is called after the terminal lock is released, so it
	// may do I/O (write to a terminal, upload a texture, update a window).
	//
	// The terminal does not serialize WriteBatch calls: they can arrive from
	// the read loop and from Resize/Restore concurrently, and in the general
	// case from different goroutines. The frontend must serialize them itself
	// (for example with its own mutex) and preserve frame order, so that the
	// memory of a newer frame is never published before an older one.
	//
	// WriteBatch may also be called when nothing was rendered: the terminal
	// calls the pair at every batch boundary, and a boundary with no
	// accumulated damage still produces a PrepareBatch that has nothing to do.
	WriteBatch()
}

BatchFrontend is an optional Frontend extension for damage-batched rendering. A frontend that implements it still receives every callback per change, but may accumulate the damage and render only when the terminal calls the batch methods.

The terminal calls the pair at these boundaries: when the read loop is about to block waiting for more input (so a lone write is still painted immediately), before the loop parks for Pause, after out-of-band screen updates such as Resize and Restore, and whenever the loop otherwise stops. After either method returns, no damage may be left unrendered.

The two calls have different locking and I/O rules, and the terminal may call WriteBatch from more than one goroutine: implementing the pair usually means rendering into a frontend-owned back buffer in PrepareBatch and publishing it, under a mutex the frontend owns, in WriteBatch. See the method comments.

type ChangeReason

type ChangeReason int

ChangeReason says what kind of change caused the region to change, for optimization etc.

const (
	// CRText means text is being printed normally.
	CRText ChangeReason = iota

	// CRClear means some area has been cleared
	CRClear

	// CRScroll means an area has been scrolled
	CRScroll

	// CRScreenSwitch means the screen has been switched between main and alt
	CRScreenSwitch

	// CRResize means the screen was resized. Content is preserved where
	// possible (not cleared), so frontends must repaint the full region
	// at the new dimensions. A dedicated reason (rather than reusing
	// CRClear) lets frontends skip clear-specific work such as resetting
	// scroll state.
	CRResize

	// CRRestore means a snapshot was restored with Terminal.Restore.
	// Frontends must repaint the full screen from the restored state.
	CRRestore

	// CRPalette means the color palette or a default color changed (OSC
	// 4/10/11/12/104/110/111/112), so every cell rendered with a palette or
	// default color may look different. Frontends that resolve colors
	// themselves must repaint the full screen.
	CRPalette
)

type ClipboardSink

type ClipboardSink interface {
	Clipboard(v LockedView, selection string, data []byte)
}

ClipboardSink is an optional Frontend extension for the OSC 52 clipboard write path. If the Frontend implements it, the base64-decoded payload of an OSC 52 sequence is forwarded via Clipboard with the requested selection (usually "c") and the raw bytes.

Clipboard is called with the terminal lock held and receives a LockedView, so implementations should read terminal state through the view and must not call back into the terminal with locking methods. Clipboard queries (data == "?") are never answered or forwarded.

type Color

type Color uint32

Color represents a terminal color value. Use the constructor functions to create values: DefaultColor, Color256, BrightColor, RGB.

func BrightColor

func BrightColor(idx int) Color

BrightColor returns a bright variant (0–7) of the standard ANSI color.

func Color256

func Color256(idx int) Color

Color256 returns a color from the 256-color palette (0–255).

func DefaultColor

func DefaultColor() Color

DefaultColor returns the terminal's default color.

func RGB

func RGB(r, g, b int) Color

RGB creates a color from red, green, and blue components (0–255 each).

func (Color) Index

func (c Color) Index() int

Index returns the 256-color or bright-color index. For RGB colors the lower 24 bits are returned.

func (Color) IsBright

func (c Color) IsBright() bool

IsBright reports whether this is a bright ANSI color (0–7).

func (Color) IsDefault

func (c Color) IsDefault() bool

IsDefault reports whether this is the terminal's default color.

func (Color) IsRGB

func (c Color) IsRGB() bool

IsRGB reports whether this is an RGB color.

func (Color) RGB

func (c Color) RGB() (r, g, b int)

RGB returns the red, green, blue components for an RGB color.

type ColorComponent

type ColorComponent uint8

ColorComponent specifies which color component of a Style to modify

const (
	ComponentFG ColorComponent = iota
	ComponentBG
	ComponentUnderline
)

type EmptyFrontend

type EmptyFrontend struct{}

EmptyFrontend is a simple frontend that does nothing

func (*EmptyFrontend) Bell

func (d *EmptyFrontend) Bell(v LockedView)

func (*EmptyFrontend) CursorMoved

func (d *EmptyFrontend) CursorMoved(v LockedView, x, y int)

func (*EmptyFrontend) RegionChanged

func (d *EmptyFrontend) RegionChanged(v LockedView, r Region, c ChangeReason)

func (*EmptyFrontend) ScrollLines

func (d *EmptyFrontend) ScrollLines(v LockedView, delta int)

func (*EmptyFrontend) StyleChanged

func (d *EmptyFrontend) StyleChanged(v LockedView, s Style)

func (*EmptyFrontend) ViewFlagChanged

func (d *EmptyFrontend) ViewFlagChanged(v LockedView, vs ViewFlag, value bool)

func (*EmptyFrontend) ViewIntChanged

func (d *EmptyFrontend) ViewIntChanged(v LockedView, vs ViewInt, value int)

func (*EmptyFrontend) ViewStringChanged

func (d *EmptyFrontend) ViewStringChanged(v LockedView, vs ViewString, value string)

type Frontend

type Frontend interface {
	Bell(v LockedView)
	RegionChanged(v LockedView, r Region, c ChangeReason)

	// ScrollLines is called for every scroll of the main (not alternate)
	// screen with the signed delta. Negative means content moved up (rows
	// left the top); positive means content moved down. Scrollback capture
	// and ScrollbackRowsAppended are separate.
	ScrollLines(v LockedView, delta int)
	CursorMoved(v LockedView, x, y int)
	StyleChanged(v LockedView, s Style)
	ViewFlagChanged(v LockedView, vs ViewFlag, value bool)
	ViewIntChanged(v LockedView, vs ViewInt, value int)
	ViewStringChanged(v LockedView, vs ViewString, value string)
}

Frontend is a type that can display data from a Terminal.

All Frontend methods are called with the terminal lock held and receive a LockedView as their first argument. The view is read-only and valid only for the duration of the callback: use it to read terminal state (Size, Line, Scrollback, ...) without locking. Do not retain it, and do not call Terminal write methods or read methods that take the lock themselves (plain Size/Line/Snapshot/... or the Terminal.Scrollback reader) from a callback, as that would deadlock. Use v.Snapshot() and v.Scrollback().Snapshot() to snapshot screen or scrollback state from a callback; Terminal.Snapshot and *Scrollback.Snapshot are for ordinary code.

type GraphemeReader

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

GraphemeReader buffers bytes and emits grapheme tokens for printable runs.

func NewGraphemeReader

func NewGraphemeReader(src io.Reader) *GraphemeReader

func NewGraphemeReaderFromState

func NewGraphemeReaderFromState(src io.Reader, st GraphemeState) *GraphemeReader

NewGraphemeReaderFromState creates a reader that resumes from st, including any buffered bytes and tokenizer state. The src is the new underlying stream.

func NewGraphemeReaderWithMode

func NewGraphemeReaderWithMode(src io.Reader, mode TextReadMode) *GraphemeReader

func (*GraphemeReader) Buffered

func (r *GraphemeReader) Buffered() int

func (*GraphemeReader) ReadByte

func (r *GraphemeReader) ReadByte() (byte, error)

func (*GraphemeReader) ReadPrintableBytes

func (r *GraphemeReader) ReadPrintableBytes(maxWidth int) (string, int, bool, error)

ReadPrintableBytes reads a contiguous run of printable bytes from the stream. It stops before control bytes and leaves them for ReadByte. maxWidth limits the total cell width returned; pass <=0 for unlimited. merge is true when the returned text should be merged into the previous cell.

func (*GraphemeReader) ReadPrintableTokens

func (r *GraphemeReader) ReadPrintableTokens(maxWidth int) ([]GraphemeToken, error)

ReadPrintableTokens reads a contiguous run of printable bytes from the stream. It stops before control bytes and leaves them for ReadByte. maxWidth limits the total cell width returned; pass <=0 for unlimited.

func (*GraphemeReader) ReadPrintableTokensInto

func (r *GraphemeReader) ReadPrintableTokensInto(maxWidth int, dst []GraphemeToken) ([]GraphemeToken, error)

ReadPrintableTokensInto is like ReadPrintableTokens but reuses dst when provided. TODO: allow a token sink for direct screen writes without slice allocation.

func (*GraphemeReader) SnapshotState

func (r *GraphemeReader) SnapshotState() GraphemeState

SnapshotState returns a deep copy of the reader's buffered bytes and tokenizer state. The receiver is not modified.

type GraphemeState

type GraphemeState struct {
	Pending []byte
	State   int
	// Initial is true when State has not been set yet; the reader then
	// starts from the tokenizer's initial state (-1) instead of state 0,
	// which would incorrectly continue a previous grapheme.
	Initial          bool
	ForceMergeNext   bool
	LastWasRI        bool
	RunHasBase       bool
	LastWasHanging   bool
	LastWasSynthetic bool
	Mode             TextReadMode
}

GraphemeState is the user-space tokenizer/parser state needed to resume a GraphemeReader in another process. Pending holds the bytes already read from the source but not yet consumed (data[start:end]).

type GraphemeToken

type GraphemeToken struct {
	Bytes []byte
	Width int
	Merge bool
}

GraphemeToken represents a grapheme cluster or a merge fragment. Bytes is a copy of the input bytes to keep tokens safe across buffer refills. When Merge is true, the token should be merged into the previous cell.

type Handoff

type Handoff struct {
	Snapshot   Snapshot
	Scrollback ScrollbackSnapshot
	Reader     ReaderState
	Size       Pos
	TextMode   TextReadMode
}

Handoff is everything needed to resume a terminal in a new process. Backend fds are transferred separately (see PTYBackend.Master/Slave).

type KeyCode

type KeyCode int
const (
	KeyRune KeyCode = iota
	KeyUp
	KeyDown
	KeyRight
	KeyLeft
	KeyHome
	KeyEnd
	KeyInsert
	KeyDelete
	KeyPageUp
	KeyPageDown
	KeyBackspace
	KeyTab
	KeyEnter
	KeyEscape
	KeyF1
	KeyF2
	KeyF3
	KeyF4
	KeyF5
	KeyF6
	KeyF7
	KeyF8
	KeyF9
	KeyF10
	KeyF11
	KeyF12
	KeyF13
	KeyF14
	KeyF15
	KeyF16
	KeyF17
	KeyF18
	KeyF19
	KeyF20
	KeyF21
	KeyF22
	KeyF23
	KeyF24
	KeyF25
	KeyF26
	KeyF27
	KeyF28
	KeyF29
	KeyF30
	KeyF31
	KeyF32
	KeyF33
	KeyF34
	KeyF35
	KeyCapsLock
	KeyScrollLock
	KeyNumLock
	KeyPrintScreen
	KeyPause
	KeyMenu
	KeyKP0
	KeyKP1
	KeyKP2
	KeyKP3
	KeyKP4
	KeyKP5
	KeyKP6
	KeyKP7
	KeyKP8
	KeyKP9
	KeyKPDecimal
	KeyKPDivide
	KeyKPMultiply
	KeyKPSubtract
	KeyKPAdd
	KeyKPEnter
	KeyKPEqual
	KeyKPSeparator
	KeyKPLeft
	KeyKPRight
	KeyKPUp
	KeyKPDown
	KeyKPPageUp
	KeyKPPageDown
	KeyKPHome
	KeyKPEnd
	KeyKPInsert
	KeyKPDelete
	KeyKPBegin
	KeyMediaPlay
	KeyMediaPause
	KeyMediaPlayPause
	KeyMediaReverse
	KeyMediaStop
	KeyMediaFastForward
	KeyMediaRewind
	KeyMediaTrackNext
	KeyMediaTrackPrev
	KeyMediaRecord
	KeyVolumeDown
	KeyVolumeUp
	KeyVolumeMute
	KeyLeftShift
	KeyLeftControl
	KeyLeftAlt
	KeyLeftSuper
	KeyLeftHyper
	KeyLeftMeta
	KeyRightShift
	KeyRightControl
	KeyRightAlt
	KeyRightSuper
	KeyRightHyper
	KeyRightMeta
	KeyISOLevel3Shift
	KeyISOLevel5Shift
)

type KeyEvent

type KeyEvent struct {
	// Code is required. Use KeyRune for text-producing keys.
	Code KeyCode

	// Rune is required when Code == KeyRune. Otherwise it is ignored.
	Rune rune

	// Mod is a bitmask of active modifiers. For modifier key events, include
	// the modifier state resulting from the event.
	Mod KeyMod

	// Event defaults to KeyPress when zero. Repeat and release require the
	// report-events enhancement.
	Event KeyEventType

	// Shifted and BaseLayout are optional alternate key codes used when the
	// report-alternates enhancement is enabled. Shifted should only be set if
	// shift is pressed.
	Shifted    rune
	BaseLayout rune

	// Text is optional associated text codepoints used when report-text is
	// enabled with report-all-keys. If empty, Rune is used for KeyRune.
	Text []rune
}

type KeyEventType

type KeyEventType uint8

KeyEventType is press/repeat/release. Zero is treated as press.

const (
	KeyPress KeyEventType = iota + 1
	KeyRepeat
	KeyRelease
)

type KeyMod

type KeyMod uint8

KeyMod is a bitmask of active modifier keys. It follows the Kitty keyboard protocol bit assignments.

const (
	ModShift KeyMod = 1 << iota
	ModAlt
	ModCtrl
	ModSuper
	ModHyper
	ModMeta
	ModCapsLock
	ModNumLock
)

type KeyboardEnhancement

type KeyboardEnhancement int

KeyboardEnhancement flags correspond to the Kitty progressive enhancement bits.

const (
	KbdDisambiguate KeyboardEnhancement = 1 << iota
	KbdReportEvents
	KbdReportAlternates
	KbdReportAllKeys
	KbdReportText
)

type Line

type Line struct {
	Spans []Span
	Width int
}

Line holds a list of spans

func (Line) Equivalent

func (l Line) Equivalent(other Line) bool

Equivalent reports whether l and other cover the same cells with the same content, style, and OSC 8 link. It is a semantic comparison: span packing is ignored, so a run of blanks stored as Span{Width: n} (empty Text) is equivalent to the same blanks stored as a Text span of spaces. Wide characters occupy their full cell width, and zero-width content (combining marks, for example) is compared in order at the same cell position rather than being dropped.

Equivalent does not compare the structural representation of the lines; use reflect.DeepEqual or cmp for that.

func (Line) PlainTextString

func (l Line) PlainTextString() string

type LockedScrollback

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

LockedScrollback is the no-lock counterpart of *Scrollback, obtained from LockedView.Scrollback. It is valid only for the duration of the callback or View that produced the view. Its methods do not lock, so they are safe to call from a Frontend callback (unlike *Scrollback methods). Its Snapshot is the callback-safe snapshot entry point; *Scrollback.Snapshot is for ordinary code.

func (LockedScrollback) Len

func (s LockedScrollback) Len() int

Len returns the number of stored scrollback rows.

func (LockedScrollback) LogicalLine

func (s LockedScrollback) LogicalLine(id ScrollID) []Row

LogicalLine returns all physical rows that form the logical line containing the given row ID. See Scrollback.LogicalLine for details on how reflown rows report IDs (one stored ID per row, can repeat).

func (LockedScrollback) MovePos

func (s LockedScrollback) MovePos(pos ScrollPos, delta int) ScrollPos

MovePos returns a new ScrollPos offset by delta physical rows from pos.

func (LockedScrollback) Range

func (s LockedScrollback) Range() (base, next ScrollID)

Range returns the IDs of the oldest and next-to-be-assigned rows.

func (LockedScrollback) Row

func (s LockedScrollback) Row(id ScrollID) (Row, bool)

Row returns the physical row with the given ID, or false if it has been evicted.

func (LockedScrollback) Snapshot

func (s LockedScrollback) Snapshot() ScrollbackSnapshot

Snapshot returns a deep copy of every stored row without taking the terminal lock. It is the callback-safe counterpart of *Scrollback.Snapshot. A LockedScrollback with no terminal returns an empty snapshot.

type LockedView

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

LockedView is a read-only handle to terminal state. It is valid only for the duration of the call that received it (a Frontend callback or Terminal.View); do not retain it. Its methods do not lock, which makes Snapshot and Scrollback (and LockedScrollback.Snapshot) the callback-safe snapshot entry points. Terminal.Snapshot and *Scrollback.Snapshot are for ordinary code and would deadlock here.

func (LockedView) ANSILine

func (v LockedView) ANSILine(y int) string

ANSILine returns the ANSI-escaped content of line y.

func (LockedView) CursorColor

func (v LockedView) CursorColor() Color

CursorColor returns the cursor color (OSC 12).

func (LockedView) CursorPos

func (v LockedView) CursorPos() Pos

CursorPos returns the cursor position.

func (LockedView) DefaultBackground

func (v LockedView) DefaultBackground() Color

DefaultBackground returns the default background color (OSC 11).

func (LockedView) DefaultForeground

func (v LockedView) DefaultForeground() Color

DefaultForeground returns the default foreground color (OSC 10).

func (LockedView) GetViewFlag

func (v LockedView) GetViewFlag(f ViewFlag) bool

GetViewFlag returns the value of a boolean terminal flag.

func (LockedView) GetViewInt

func (v LockedView) GetViewInt(i ViewInt) int

GetViewInt returns the value of an integer terminal setting.

func (LockedView) GetViewString

func (v LockedView) GetViewString(s ViewString) string

GetViewString returns the value of a string terminal setting.

func (LockedView) Line

func (v LockedView) Line(y int) string

Line returns the plain text content of line y.

func (LockedView) PaletteColor

func (v LockedView) PaletteColor(idx int) Color

PaletteColor returns the RGB color mapped to palette index idx. Indexes outside 0-255 return the default color.

func (LockedView) Scrollback

func (v LockedView) Scrollback() LockedScrollback

Scrollback returns a no-lock scrollback reader for use inside callbacks.

func (LockedView) Size

func (v LockedView) Size() (w, h int)

Size returns the terminal width and height.

func (LockedView) Snapshot

func (v LockedView) Snapshot() Snapshot

Snapshot returns a deep copy of the terminal's screen state without taking the terminal lock. It is the callback-safe counterpart of Terminal.Snapshot. Like Terminal.Snapshot, it excludes the scrollback buffer and backend/process state. A zero LockedView returns an empty Snapshot.

func (LockedView) StyledLine

func (v LockedView) StyledLine(x, w, y int) Line

StyledLine returns the styled content of a line region.

func (LockedView) StyledLines

func (v LockedView) StyledLines(r Region) []Line

StyledLines returns the styled content of multiple lines in a region.

type Mode

type Mode uint16

Mode represents a text styling mode (bold, italic, underline, etc.)

const (
	ModeBold Mode = 1 << iota
	ModeDim
	ModeItalic
	ModeUnderline
	ModeBlink
	ModeReverse
	ModeInvisible
	ModeStrike
	ModeOverline
	ModeDoubleUnderline
	ModeFramed
	ModeEncircled
	ModeRapidBlink
)

type MouseBtn

type MouseBtn byte

type MouseFlag

type MouseFlag byte

type NoPTYBackend

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

NoPTYBackend implements Backend using provided IO streams.

func NewNoPTYBackend

func NewNoPTYBackend(r io.Reader, w io.Writer) *NoPTYBackend

NewNoPTYBackend returns a backend using the provided reader and writer.

func (*NoPTYBackend) Close

func (b *NoPTYBackend) Close() error

func (*NoPTYBackend) Read

func (b *NoPTYBackend) Read(p []byte) (int, error)

func (*NoPTYBackend) SetReadDeadline

func (b *NoPTYBackend) SetReadDeadline(t time.Time) error

SetReadDeadline forwards to the underlying reader when it supports deadlines (for example *os.File or an os.Pipe). Otherwise it returns errors.ErrUnsupported. Terminal.Pause then cannot interrupt a blocked read until the next byte arrives.

func (*NoPTYBackend) SetSize

func (b *NoPTYBackend) SetSize(w, h int) error

func (*NoPTYBackend) Write

func (b *NoPTYBackend) Write(p []byte) (int, error)

type NotificationSink

type NotificationSink interface {
	Notify(v LockedView, title, body string)
}

NotificationSink is an optional Frontend extension for OSC 9 and OSC 777 desktop notifications. OSC 9 carries a body only (title is empty); OSC 777 carries "notify;title;body". It is called with the terminal lock held.

type PTYBackend

type PTYBackend struct {

	// AutoEnv lists the environment variables Attach injects into commands,
	// in "KEY=VALUE" form. nil applies the defaults (TERM and COLORTERM);
	// set it to an empty non-nil slice to inject nothing, or to a custom
	// list to override the defaults and add your own variables. Injected
	// values replace existing entries with the same key in cmd.Env.
	AutoEnv []string
	// contains filtered or unexported fields
}

PTYBackend implements Backend using github.com/creack/pty. A single pty pair is shared by every command attached to the backend, so commands can be run back-to-back through the same terminal state.

func (*PTYBackend) Adopt

func (p *PTYBackend) Adopt(master, slave *os.File) error

Adopt adopts an already-open pty pair. master is required; slave may be nil when only the master is available. The backend takes ownership of the files (Close closes them). Call before constructing the Terminal or opening the pty backend. With slave == nil, Attach is unavailable and Read does not wait for reuse on EOF/EIO.

Use os.NewFile(fd, "pts") to wrap raw pty file descriptors before calling Adopt.

func (*PTYBackend) AdoptMaster

func (p *PTYBackend) AdoptMaster(master *os.File) error

AdoptMaster is Adopt(master, nil). With no slave, Open and Attach are unavailable and Read surfaces EOF/EIO instead of waiting for reuse.

func (*PTYBackend) Attach

func (p *PTYBackend) Attach(c *exec.Cmd) error

Attach wires an *exec.Cmd to run inside the PTY. The PTY pair is opened lazily on first call and reused on subsequent calls. Caller starts the command themselves via cmd.Start().

func (*PTYBackend) Close

func (p *PTYBackend) Close() error

Close shuts down the pty pair so a blocked Read returns promptly, and prevents any later lazy reopen: subsequent Read/Write/Attach calls fail with io.ErrClosedPipe. It is idempotent.

func (*PTYBackend) Master

func (p *PTYBackend) Master() *os.File

Master returns the pty master file. The backend still owns it; do not close it while handing it off. It returns nil if the pty cannot be opened (for example after Close).

func (*PTYBackend) Open

func (p *PTYBackend) Open() (*os.File, error)

Open creates a new pty pair and returns the slave for external use. The pair is created lazily, so it is safe to call this before or after constructing a Terminal. It returns an error when the backend was created by Adopt with a nil slave, because there is nothing to return or attach.

func (*PTYBackend) PID

func (p *PTYBackend) PID() int

PID returns the process ID of the most recently attached command, or 0 if no command has been attached or it has not been started yet. It is safe to call from any goroutine.

func (*PTYBackend) Read

func (p *PTYBackend) Read(b []byte) (int, error)

Read reads from the pty master. When the current child has exited the master returns syscall.EIO on Linux and io.EOF on some platforms (Darwin); because the pty stays valid for the next attached command, Read waits for one instead of surfacing that as EOF. That reuse behavior requires a slave: for an adopted master-only pty, Read performs a plain master read and surfaces EOF/EIO so the read loop can exit. After Close, Read returns io.ErrClosedPipe instead of retrying. NoPTYBackend keeps EOF terminal.

func (*PTYBackend) SetReadDeadline

func (p *PTYBackend) SetReadDeadline(t time.Time) error

SetReadDeadline sets a read deadline on the pty master. It is used by Terminal.Pause to interrupt a blocked read.

func (*PTYBackend) SetSize

func (p *PTYBackend) SetSize(w, h int) error

func (*PTYBackend) Slave

func (p *PTYBackend) Slave() *os.File

Slave returns the pty slave file, or nil when the backend was adopted with only a master (see AdoptMaster). The backend still owns it; do not close it while handing it off.

func (*PTYBackend) Wait

func (p *PTYBackend) Wait() error

Wait waits for the most recently attached command to exit and returns the error from (*exec.Cmd).Wait. It errors if no command has been attached.

The caller must not also call cmd.Wait() on the attached command: like os/exec, only one Wait may consume the process result. A second call to PTYBackend.Wait returns an "exec: Wait was already called" error. Wait may be called after Close (the child still needs reaping) and after a later Attach, in which case it waits for that newer command.

func (*PTYBackend) Wake

func (p *PTYBackend) Wake()

Wake interrupts a Read that is waiting for the next attached command, so a Pause does not have to wait out the reuse backoff. It is safe to call from any goroutine.

func (*PTYBackend) Write

func (p *PTYBackend) Write(b []byte) (int, error)

type Pos

type Pos struct {
	X int
	Y int
}

Pos holds X/Y coordinates.

type PromptMark

type PromptMark struct {
	Kind PromptMarkKind
	// ExitCode is the status reported by OSC 133;D, or -1 when absent (it is
	// always -1 for the other kinds).
	ExitCode int
	// Raw is the full parameter string that followed the kind letter, for
	// marks that carry extension data this package does not decode.
	Raw string
}

PromptMark is a decoded OSC 133 shell-integration mark.

type PromptMarkKind

type PromptMarkKind int

PromptMarkKind identifies one of the OSC 133 semantic prompt marks that shells and prompt frameworks emit for shell integration.

const (
	// PromptMarkStart is OSC 133;A: the prompt is about to be drawn.
	PromptMarkStart PromptMarkKind = iota
	// PromptMarkInput is OSC 133;B: the prompt ended and user input starts.
	PromptMarkInput
	// PromptMarkCommand is OSC 133;C: the command was accepted and its
	// output starts.
	PromptMarkCommand
	// PromptMarkDone is OSC 133;D: the command finished. ExitCode carries
	// the reported status, or -1 when the sequence did not include one.
	PromptMarkDone
)

type ReaderState

type ReaderState struct {
	Pending        []byte // GraphemeReader buffered bytes (data[start:end])
	PendingCommand []byte // partial ESC-initiated command (ESC already consumed)
	State          int    // GraphemeReader UTF-8/grapheme state
	// Initial marks a reader that has not processed any bytes yet; State is
	// then ignored and the reader starts from the tokenizer's initial state.
	Initial          bool
	ForceMergeNext   bool
	LastWasRI        bool
	RunHasBase       bool
	LastWasHanging   bool
	LastWasSynthetic bool
	Mode             TextReadMode
}

ReaderState is the read-loop state that lives in user space: bytes already read from the backend but not applied, plus any partial escape command.

type Region

type Region struct {
	X, Y, X2, Y2 int
}

Region is an non-inclusive rectangle on the screen. X2 and Y2 are not included in the region.

func (Region) Add

func (r Region) Add(x, y int) Region

func (Region) Clamp

func (r Region) Clamp(rc Region) Region

func (Region) Empty

func (r Region) Empty() bool

Empty reports whether the region has no area.

func (Region) Intersect

func (r Region) Intersect(o Region) Region

Intersect returns the overlapping portion of two regions.

type Row

type Row struct {
	Spans   []Span
	Width   int
	Wrapped bool
	ID      ScrollID
}

Row represents a single physical row from the scrollback buffer. Spans is a copy owned by the caller: mutating it does not affect the stored history.

func (Row) Equivalent

func (r Row) Equivalent(other Row) bool

Equivalent reports whether r and other cover the same cells with the same content, style, and OSC 8 link. Wrapped and ID are ignored; see Line.Equivalent for the cell comparison semantics.

func (Row) PlainTextString

func (r Row) PlainTextString() string

PlainTextString returns the row's text with styling dropped.

type ScreenState

type ScreenState struct {
	Width, Height    int
	Lines            []Line // full-width rows; Spans are copies
	Wrapped          []bool // auto-wrap continuation flags, len == Height
	Cursor           Pos
	PendingWrap      bool // next printable write wraps before writing
	SavedCursor      Pos
	SavedPendingWrap bool // pending-wrap state saved by DECSC
	SavedStyle       Style
	// SavedCharsets is the G0-G3/GL state saved by DECSC; Charsets is the
	// current character set state.
	SavedCharsets charsetState
	Charsets      charsetState
	// InsertMode is IRM; OriginMode/SavedOriginMode are DECOM and its DECSC
	// slot. LastChar is the last graphic code point replayed by REP (0 means
	// none). Tabs is the horizontal tab stop map (nil means the default stops
	// every 8 columns).
	InsertMode      bool
	OriginMode      bool
	SavedOriginMode bool
	LastChar        rune
	Tabs            []bool
	Style           Style
	TopMargin       int
	BottomMargin    int
	AutoWrap        bool
}

ScreenState is a deep copy of one screen buffer.

type ScrollID

type ScrollID uint64

ScrollID is a monotonically increasing identifier for a physical row in the scrollback buffer. IDs are globally unique per terminal and stable even as new lines are added or old lines are evicted. Use Range to discover the set of valid IDs.

type ScrollPos

type ScrollPos struct {
	ID ScrollID
}

ScrollPos identifies a position within the scrollback by its physical row ID.

type Scrollback

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

Scrollback provides read access to the terminal's scrollback buffer. All methods handle their own locking and are safe to call from any goroutine, except from within a Frontend callback: callbacks run with the terminal lock already held, so calling any Scrollback method there deadlocks. Collect what you need before/after instead.

Rows handed out by Row and LogicalLine own their Spans slices (copies), so callers may freely mutate the returned Rows.

func (*Scrollback) Clear

func (s *Scrollback) Clear()

Clear drops all stored scrollback rows. ScrollIDs stay monotonic: after Clear, Range reports base == next == the current counter, and subsequently captured rows continue from that counter (no ID is ever reused).

func (*Scrollback) Len

func (s *Scrollback) Len() int

Len returns the number of stored scrollback rows.

func (*Scrollback) LogicalLine

func (s *Scrollback) LogicalLine(id ScrollID) []Row

LogicalLine returns all physical rows that form the logical line containing the given row ID. The rows are reflown at the terminal's current width, so the number and sizes of returned rows may differ from the captured storage. Returns nil if the ID has been evicted. The returned Rows own their span slices; mutating them does not affect the stored history.

Every returned Row carries the ScrollID of the stored physical row that contains its first cell. When reflow splits a stored row across several reflown rows they share that stored row's ID, and because reflown rows may straddle a boundary between two stored rows, the reported IDs are non-decreasing and may repeat but are always non-zero. Row(id) still works for each reported ID; it returns the stored (captured) row, not the reflown one.

func (*Scrollback) MovePos

func (s *Scrollback) MovePos(pos ScrollPos, delta int) ScrollPos

MovePos returns a new ScrollPos offset by delta physical rows from pos. Negative delta moves toward older rows; positive moves toward newer ones. If delta would go out of bounds, the result is clamped to the valid range. Note: MovePos operates on the stored physical row count, not on reflown rows. Use LogicalLine to discover how a stored row expands at the current width.

func (*Scrollback) Range

func (s *Scrollback) Range() (base, next ScrollID)

Range returns the IDs of the oldest and next-to-be-assigned rows. Valid IDs satisfy base <= id < next.

func (*Scrollback) Restore

func (s *Scrollback) Restore(snap ScrollbackSnapshot) error

Restore replaces the scrollback contents with snap. The snapshot's Base and Next IDs are preserved, so IDs continue monotonically from where the snapshot left off. If snap has more rows than the current capacity, only the newest rows are kept and Base is advanced accordingly. Restore returns ErrScrollbackSnapshotInvalid without changing the buffer when the snapshot is malformed (non-contiguous IDs, Next/Base inconsistent with the row count, or a zero row ID).

func (*Scrollback) Row

func (s *Scrollback) Row(id ScrollID) (Row, bool)

Row returns the physical row with the given ID, or false if it has been evicted. The returned Row owns a copy of the stored spans.

func (*Scrollback) Snapshot

func (s *Scrollback) Snapshot() ScrollbackSnapshot

Snapshot returns a deep copy of every stored row, oldest to newest, along with the ID range. Mutating the snapshot does not affect the buffer. Scrollback snapshots are independent of Terminal.Snapshot, which excludes the scrollback buffer; use both when restoring a full terminal session.

type ScrollbackListener

type ScrollbackListener interface {
	ScrollbackRowsAppended(v LockedView, first, next ScrollID)
}

ScrollbackListener is an optional Frontend extension. When rows are appended to the main-screen scrollback, ScrollbackRowsAppended is called once per capture batch with the terminal lock held and the range [first, next) of newly assigned ScrollIDs. Use the LockedView (including v.Scrollback()) to read the rows; do not retain the view. Alternate-screen output never produces rows. Scrollback.Clear() does not call this.

type ScrollbackSnapshot

type ScrollbackSnapshot struct {
	Rows []Row    // oldest -> newest, deep copies
	Base ScrollID // ID of Rows[0]
	Next ScrollID // next ID to assign
}

ScrollbackSnapshot is a deep copy of a scrollback buffer. Rows are ordered oldest to newest; Base is the ID of Rows[0] and Next is the next ID that would be assigned. It is separate from Terminal.Snapshot/Restore, which deliberately exclude the scrollback buffer.

type ShellIntegration

type ShellIntegration interface {
	PromptMark(v LockedView, mark PromptMark)
}

ShellIntegration is an optional Frontend extension for OSC 133 semantic prompt marks (shell integration). It is called with the terminal lock held; read state through the view and do not call locking methods on the terminal. Unknown OSC 133 marks (for example 133;P properties) are not forwarded.

type Snapshot

type Snapshot struct {
	Main, Alt    ScreenState
	OnAltScreen  bool
	ViewFlags    []bool
	ViewInts     []int
	ViewStrings  []string
	KeyboardMain keyboardMode
	KeyboardAlt  keyboardMode
	Link         string // active OSC 8 hyperlink, if any
	NewlineMode  bool   // LNM (CSI 20 h/l)
	// Palette and default colors are resolved by frontends rather than stored
	// in cells, so they travel with the snapshot.
	Palette       [paletteSize]Color
	DefaultFG     Color
	DefaultBG     Color
	DefaultCursor Color
}

Snapshot is a deep copy of terminal state. It excludes the scrollback buffer and backend/process state.

type Span

type Span struct {
	Style Style // Complete text styling (colors and modes)
	Text  string
	Width int
	// Link is the OSC 8 hyperlink URI active when the span was written.
	// Empty means no link. Blank/erased spans never carry a link.
	Link string
}

Span represents a run of text with consistent styling. If Text is not empty, it contains the text content. If Text is empty, the span represents Width spaces; a Width of zero (or less) is empty.

type Style

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

Style represents complete text styling including colors and text modes. Modes are distributed across the high bytes (bits 24-30) of fg, bg, and underlineColor. Color data uses bits 0-23, and bit 31 is the color type flag (256-color vs RGB).

func NewStyle

func NewStyle() Style

NewStyle creates a default style with no colors and no modes set

func (Style) ANSIEscape

func (s Style) ANSIEscape() []byte

ANSIEscape generates a complete ANSI escape sequence for this style

func (Style) ANSIEscapeFrom

func (s Style) ANSIEscapeFrom(prev Style) []byte

ANSIEscapeFrom generates a minimal ANSI escape sequence that changes from prev to this style

func (*Style) GetColor

func (s *Style) GetColor(component ColorComponent) (Color, error)

GetColor returns the Color for a component.

func (*Style) GobDecode

func (s *Style) GobDecode(data []byte) error

GobDecode decodes a style previously produced by GobEncode.

func (Style) GobEncode

func (s Style) GobEncode() ([]byte, error)

GobEncode encodes the style as its packed internal representation.

func (Style) MarshalJSON

func (s Style) MarshalJSON() ([]byte, error)

MarshalJSON encodes the style as its packed internal representation. The numbers are not palette indexes: they are Style's raw uint32 fields (color data in bits 0-23, modes in bits 24-30, RGB flag in bit 31).

func (*Style) Modes

func (s *Style) Modes() []Mode

Modes returns a slice of all currently set modes

func (*Style) Reset

func (s *Style) Reset(modes ...Mode)

Reset clears specific text modes, or resets everything when no modes are given.

style.Reset()           // reset all colors and modes
style.Reset(ModeBold)   // clear only bold

func (*Style) SetColor

func (s *Style) SetColor(component ColorComponent, color Color) error

SetColor sets a color component to the given Color value.

func (*Style) SetMode

func (s *Style) SetMode(modes ...Mode)

SetMode sets one or more text modes

func (*Style) TestMode

func (s *Style) TestMode(mode Mode) bool

TestMode checks if a mode is set

func (*Style) UnmarshalJSON

func (s *Style) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a style previously produced by MarshalJSON. A JSON null leaves the style unchanged.

type TTYFrontend

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

TTYFrontend renders changed regions to a terminal (tty) output. It can be attached to a region of the screen and detached to stop updates.

Rendering is damage-batched: RegionChanged unions the changed area into a pending frame instead of repainting immediately. The terminal calls PrepareBatch/WriteBatch (see BatchFrontend) at its batch boundaries, so a burst of escape-dense output costs one render pass and one write instead of one per callback. Rows whose rendered bytes match the previous frame are skipped, which makes an unchanged full-screen repaint nearly free.

Lock ordering: the terminal lock always ranks above the frontend mutex. Callback methods (invoked with the terminal lock held) may take t.mu, but public methods must never hold t.mu while taking the terminal lock. Attach snapshots state under t.mu, releases it, then renders under the terminal lock. writeMu is a leaf lock serializing output writes; it is never taken while the terminal lock is held.

func NewTTYFrontend

func NewTTYFrontend(out io.Writer) *TTYFrontend

NewTTYFrontend returns a frontend that writes to out (defaults to stdout). The terminal is set automatically when the Terminal is created with NewWithMode.

func (*TTYFrontend) Attach

func (t *TTYFrontend) Attach(r Region)

Attach starts updating the provided region.

Locking: this takes t.mu only to update fields and snapshot state, then releases it before taking the terminal lock to render. It must never hold t.mu while taking the terminal lock: RegionChanged runs with the terminal lock held and takes t.mu, so the opposite order would deadlock.

func (*TTYFrontend) Bell

func (t *TTYFrontend) Bell(v LockedView)

func (*TTYFrontend) Blur

func (t *TTYFrontend) Blur()

Blur disables cursor updates and restores cursor visibility.

func (*TTYFrontend) CursorMoved

func (t *TTYFrontend) CursorMoved(v LockedView, x, y int)

func (*TTYFrontend) Detach

func (t *TTYFrontend) Detach()

Detach stops updating the attached region.

func (*TTYFrontend) Focus

func (t *TTYFrontend) Focus()

Focus enables cursor updates and visibility for this frontend.

func (*TTYFrontend) PrepareBatch

func (t *TTYFrontend) PrepareBatch(view LockedView)

PrepareBatch renders all damage accumulated since the last frame into the pending output buffer. It is called with the terminal lock held and does no I/O. Implements BatchFrontend.

func (*TTYFrontend) RegionChanged

func (t *TTYFrontend) RegionChanged(view LockedView, r Region, reason ChangeReason)

func (*TTYFrontend) ScrollLines

func (t *TTYFrontend) ScrollLines(v LockedView, y int)

func (*TTYFrontend) SetFocus

func (t *TTYFrontend) SetFocus(focused bool)

SetFocus sets focus state. When unfocused, cursor updates are suppressed.

func (*TTYFrontend) SetTerminal

func (t *TTYFrontend) SetTerminal(term Terminal)

SetTerminal updates the terminal used for rendering.

func (*TTYFrontend) StyleChanged

func (t *TTYFrontend) StyleChanged(v LockedView, s Style)

func (*TTYFrontend) ViewFlagChanged

func (t *TTYFrontend) ViewFlagChanged(v LockedView, flag ViewFlag, value bool)

func (*TTYFrontend) ViewIntChanged

func (t *TTYFrontend) ViewIntChanged(v LockedView, flag ViewInt, value int)

func (*TTYFrontend) ViewStringChanged

func (t *TTYFrontend) ViewStringChanged(v LockedView, flag ViewString, value string)

func (*TTYFrontend) WriteBatch

func (t *TTYFrontend) WriteBatch()

WriteBatch writes the output prepared by PrepareBatch. It is called without the terminal lock; concurrent calls are serialized by writeMu and preserve frame order. Implements BatchFrontend.

type TeeBackend

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

TeeBackend duplicates all reads into tee.

func NewTeeBackend

func NewTeeBackend(backend Backend) *TeeBackend

NewTeeBackend returns a TeeBackend wrapping the provided backend.

func (*TeeBackend) Close

func (t *TeeBackend) Close() error

Close forwards to the wrapped backend when it implements io.Closer.

func (*TeeBackend) Read

func (t *TeeBackend) Read(p []byte) (int, error)

func (*TeeBackend) SetReadDeadline

func (t *TeeBackend) SetReadDeadline(d time.Time) error

func (*TeeBackend) SetSize

func (t *TeeBackend) SetSize(w, h int) error

func (*TeeBackend) SetTee

func (t *TeeBackend) SetTee(w io.Writer)

SetTee updates the tee writer.

func (*TeeBackend) Wake

func (t *TeeBackend) Wake()

SetReadDeadline forwards to the wrapped backend when it supports deadlines. Wake forwards to the wrapped backend when it implements Wake.

func (*TeeBackend) Write

func (t *TeeBackend) Write(p []byte) (int, error)

type Terminal

type Terminal interface {
	SetFrontend(f Frontend)

	// View runs fn with a read-only view of terminal state while holding
	// the terminal lock. Use it for atomic multi-reads. The view is only
	// valid for the duration of fn; do not retain it. Do not call the
	// locking read methods (Size, Line, Scrollback, ...) from fn.
	View(func(LockedView))

	Write(b []byte) (int, error)
	SendKey(KeyEvent) (int, error)
	SendMouseRaw(btn MouseBtn, press bool, mods MouseFlag, x, y int) error
	Resize(int, int) error

	// Size returns the terminal width and height. It takes the terminal
	// lock itself, so it is safe from any goroutine but must not be called
	// from inside View or a Frontend callback (use the view there).
	Size() (int, int)
	// CursorPos returns the cursor position. It locks internally; use the
	// view from callbacks.
	CursorPos() Pos
	// Line returns the plain text content of line y. It locks internally;
	// use the view from callbacks.
	Line(int) string
	// ANSILine returns the ANSI-escaped content of line y. It locks
	// internally; use the view from callbacks.
	ANSILine(y int) string
	// StyledLine returns the styled content of a line region. It locks
	// internally; use the view from callbacks.
	StyledLine(x, w, y int) Line
	// StyledLines returns the styled content of multiple lines in a region.
	// It locks internally; use the view from callbacks.
	StyledLines(r Region) []Line

	// GetViewFlag, GetViewInt and GetViewString return terminal view
	// settings. They lock internally; use the view from callbacks.
	GetViewFlag(ViewFlag) bool
	GetViewInt(ViewInt) int
	GetViewString(ViewString) string

	// PaletteColor returns the RGB color mapped to a palette index (OSC 4),
	// and the default color accessors report the OSC 10/11/12 colors. They
	// lock internally; use the LockedView counterparts in callbacks.
	PaletteColor(idx int) Color
	DefaultForeground() Color
	DefaultBackground() Color
	CursorColor() Color

	// Scrollback returns the terminal's scrollback buffer for ordinary
	// code. Its methods take the terminal lock themselves, so they must
	// not be called from inside View or a Frontend callback. Use
	// LockedView.Scrollback there instead.
	Scrollback() *Scrollback

	// Snapshot returns a deep copy of the terminal's screen state. It
	// locks internally, so it must not be called from inside View or a
	// Frontend callback (use LockedView.Snapshot there). The scrollback
	// buffer and backend/process state are not included. Mutating the
	// returned Snapshot does not affect the terminal, and vice versa.
	Snapshot() Snapshot
	// Restore replaces the terminal's screen state with a previously
	// captured Snapshot and emits a full repaint. It locks internally. A
	// snapshot of a different size is resized to the snapshot dimensions
	// and the backend is resized too. The scrollback buffer is untouched.
	Restore(Snapshot) error

	// Close shuts down the terminal: it closes the backend (if it
	// implements io.Closer) so a blocked Read returns and the read loop
	// exits. It is idempotent and never blocks waiting for the loop.
	Close() error

	// Pause stops the read loop, captures the user-space reader/parser
	// state plus a snapshot and scrollback snapshot, and leaves the
	// terminal paused. Resume continues a same-process paused loop.
	// For a cross-process handoff, Pause then transfer the backend fds
	// (see PTYBackend.Master/Slave) and close this terminal.
	Pause(context.Context) (Handoff, error)
	Resume() error

	// Done is closed when the read loop exits.
	Done() <-chan struct{}
	// Err reports how the read loop terminated: nil for a clean EOF,
	// otherwise the error that stopped the loop. It is only meaningful
	// after Done is closed.
	Err() error
}

func New

func New(f Frontend, backend Backend) Terminal

New makes a new terminal using the provided Frontend, Backend, and default text read mode. The default mode is TextReadModeGrapheme, which is the correct terminal behavior for emoji, combining marks and other grapheme clusters. Use NewWithMode to select TextReadModeRune explicitly.

func NewFromHandoff

func NewFromHandoff(f Frontend, backend Backend, h Handoff, opts ...TerminalOption) (Terminal, error)

NewFromHandoff creates a terminal from a Handoff and starts its read loop with the captured reader state. The terminal is created paused (no reads happen) until the snapshot and scrollback have been restored. Handoff.Size is the target size; if the snapshot was captured at a different size its content is resized to fit. The caller owns backend and must arrange any cross-process fd transfer beforehand (PTYBackend.Adopt/AdoptMaster). On a restore error the backend is closed.

func NewWithMode

func NewWithMode(f Frontend, backend Backend, mode TextReadMode, opts ...TerminalOption) Terminal

NewWithMode makes a new terminal using the provided Frontend, Backend, and text read mode. If the frontend implements SetTerminal (e.g. *TTYFrontend), it is wired automatically. Optional TerminalOptions configure scrollback size and other settings.

type TerminalOption

type TerminalOption func(*terminal)

TerminalOption configures a Terminal at construction time.

func WithScrollback

func WithScrollback(rows int) TerminalOption

WithScrollback sets the scrollback row capacity. Rows <= 0 uses DefaultScrollbackRows.

type TextReadMode

type TextReadMode int

TextReadMode controls how printable text is tokenized from the input stream.

const (
	// TextReadModeRune emits one token per rune. Zero-width runes (combining
	// marks, VS16, ZWJ, emoji modifiers) merge into the previous cell, but
	// rune mode does not cluster whole ZWJ sequences: a ZWJ family such as
	// "👨\u200d👩\u200d👧\u200d👦" occupies 8 cells (each person is width 2)
	// while the same sequence is 2 cells in grapheme mode.
	TextReadModeRune TextReadMode = iota
	// TextReadModeGrapheme emits one token per grapheme cluster.
	TextReadModeGrapheme
)

type ViewFlag

type ViewFlag int

ViewFlag is an enum of boolean flags on a terminal

const (
	VFBlinkCursor ViewFlag = iota
	VFShowCursor
	VFReportFocus
	VFBracketedPaste
	VFAppCursorKeys
	VFAppKeypad
	// VFAltScroll tracks DECSET/DECRST 1007 (alternate scroll mode): on the
	// alternate screen, with no mouse mode active, wheel events are sent to
	// the application as cursor up/down keys instead of mouse reports.
	VFAltScroll
	// VFMousePixel tracks DECSET/DECRST 1016 (SGR-Pixels mouse reporting).
	// While set, the frontend should pass pixel coordinates to SendMouseRaw;
	// the coordinates are encoded verbatim (SGR-Pixels pairs with mode 1006).
	VFMousePixel
	// VFAltScreenAllowed tracks DEC private mode 1046. While clear, requests
	// to switch to the alternate screen (?47/?1047/?1049) are ignored.
	VFAltScreenAllowed
	// VFSynchronizedOutput tracks DECSET/DECRST 2026 (synchronized output).
	// While set, the application is emitting a batch of updates and
	// frontends may defer repaints until it is cleared. Damage batching
	// itself is opt-in through BatchFrontend.
	VFSynchronizedOutput
)

type ViewInt

type ViewInt int

ViewInt is an enum of integer settings on a terminal

const (
	VIMouseMode ViewInt = iota
	VIMouseEncoding
	VIModifyOtherKeys
	// VICursorShape is DECSCUSR (CSI Ps SP q), stored as the raw Ps value.
	VICursorShape
)

type ViewString

type ViewString int

ViewString is an enum of string settings on a terminal

const (
	VSWindowTitle ViewString = iota
	VSCurrentDirectory
	VSCurrentFile
	VSWindowIcon
)

ViewString identifiers are stored positionally in Snapshot.ViewStrings, so new values must be appended at the end of this block. Do not renumber or insert constants in the middle.

Directories

Path Synopsis
examples
esctest command
simple command
tmux command
tty command
unicode_test command

Jump to

Keyboard shortcuts

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