A VT220-compatible terminal emulator for Go that processes ANSI escape sequences and maintains terminal state without a display. You feed it bytes, it updates internal buffers, cursor position, colors, and modes. Useful for parsing terminal output, testing ANSI applications, or building terminal UIs.
- Parsing terminal output: Process ANSI-colored output from commands and extract text/formatting
- ANSI testing: Verify that applications produce correct escape sequences
- Terminal UI backends: Build headless terminal interfaces that can be rendered later
- Log analysis: Parse ANSI-formatted logs while preserving structure and colors
- Output capture: Record complete terminal state including scrollback for replay
- Interactive terminals: This is not a PTY. It doesn't handle input, process management, or shell interaction
- Real terminal emulators: No rendering, no window management, no user interaction
- Simple text extraction: If you just need plain text, use
stringsor regex instead - Live terminal sessions: This processes static byte streams, not bidirectional communication
- Full VT100+ compatibility: Focuses on VT220 subset; some edge cases may differ
The library processes a stream of bytes incrementally:
Raw bytes → ANSI decoder → Handler methods → State updates
State model:
- Two buffers: Primary (with scrollback) and alternate (no scrollback, for full-screen apps)
- Active buffer: Switches automatically when entering/exiting alternate screen mode
- Cell grid: 2D array where each cell stores character, colors, and formatting flags
- Cursor: Tracks position, visibility, and style
- Cell template: Default attributes (colors, bold, etc.) applied to new characters
- Modes: Bitmask of terminal behaviors (line wrap, insert mode, origin mode, etc.)
Flow:
- You call
Write()orWriteString()with raw bytes - Internal decoder parses ANSI sequences and calls handler methods (e.g.,
Input(),Goto(),SetTerminalCharAttribute()) - Handlers update buffers, cursor, colors, or modes
- You read state via
Cell(),CursorPos(),String()
Thread safety: All public methods are safe for concurrent use (internal locking).
Providers: Optional callbacks for external events (bell, title changes, clipboard, etc.). Default to no-ops.
go get github.com/danielgatis/go-headless-termpackage main
import (
"fmt"
headlessterm "github.com/danielgatis/go-headless-term"
)
func main() {
term := headlessterm.New()
// Write ANSI sequences
term.WriteString("\x1b[31mHello ") // Red text
term.WriteString("\x1b[32mWorld") // Green text
term.WriteString("\x1b[0m!\r\n") // Reset and newline
// Read terminal content
fmt.Println(term.String())
// Get cursor position
row, col := term.CursorPos()
fmt.Printf("Cursor: row=%d, col=%d\n", row, col)
}The repository includes working examples in the examples/ directory:
Demonstrates basic terminal operations: writing ANSI sequences, reading content, and checking cursor position.
cd examples/basic
go run main.goShows:
- Setting terminal title (OSC 0)
- Text colors and formatting (SGR codes)
- Screen clearing
- Reading terminal state
The main type. Created with New() and configured via options:
term := headlessterm.New(
headlessterm.WithSize(24, 80),
headlessterm.WithAutoResize(),
headlessterm.WithScrollback(storage),
)Key methods:
Write([]byte)/WriteString(string): Process raw bytes (implementsio.Writer)Cell(row, col): Get cell at position (returns*Cellornil)CursorPos(): Get cursor position (0-based)String(): Get visible screen content as textResize(rows, cols): Change dimensionsIsAlternateScreen(): Check if alternate buffer is active
Stores the 2D cell grid. Two buffers exist:
- Primary: Has scrollback (lines scrolled off top are saved)
- Alternate: No scrollback (cleared when switching back)
Access via Terminal.Cell() (reads from active buffer).
Represents one grid position:
Char: The rune (character)Fg/Bg: Foreground/background colors (color.Color)Flags: Bitmask (bold, underline, reverse, etc.)Hyperlink: Optional OSC 8 hyperlinkIsWide(): True if character occupies 2 columns (CJK, emoji)IsWideSpacer(): True if this is the second cell of a wide character
Configure terminal at creation:
WithSize(rows, cols): Set dimensions (default: 24x80)WithAutoResize(): Buffer grows instead of scrolling/wrappingWithScrollback(provider): Custom scrollback storageWithResponse(writer): Writer for terminal responses (DSR, etc.)WithBell(provider): Handler for bell eventsWithTitle(provider): Handler for title changesWithClipboard(provider): Handler for OSC 52 clipboardWithNotification(provider): Handler for OSC 99 desktop notifications (Kitty protocol)WithMiddleware(mw): Intercept handler calls
Interfaces for external events (all optional, default to no-ops):
BellProvider: Called on BEL (0x07)TitleProvider: Called on OSC 0/1/2 (title changes)ClipboardProvider: Called on OSC 52 (clipboard read/write)ScrollbackProvider: Stores lines scrolled off topRecordingProvider: Captures raw input bytesNotificationProvider: Called on OSC 99 (desktop notifications, Kitty protocol)
Cells track modification state:
HasDirty(): True if any cell modified since lastClearDirty()DirtyCells(): List of modified positionsClearDirty(): Reset tracking
Useful for incremental rendering (only redraw changed cells).
The terminal supports the Kitty desktop notification protocol (OSC 99). Implement NotificationProvider to handle notifications:
type MyNotificationHandler struct{}
func (h *MyNotificationHandler) Notify(payload *headlessterm.NotificationPayload) string {
// Handle the notification
fmt.Printf("Notification: %s\n", string(payload.Data))
// For query requests, return capabilities
if payload.PayloadType == "?" {
return "\x1b]99;i=test;p=title:body\x1b\\"
}
return ""
}
term := headlessterm.New(
headlessterm.WithNotification(&MyNotificationHandler{}),
)The NotificationPayload contains:
ID: Unique identifier for chunking/trackingPayloadType: Type of data ("title", "body", "icon", "?", etc.)Data: Payload content (decoded if base64)Urgency: 0 (low), 1 (normal), 2 (critical)Sound: Notification sound ("system", "silent", etc.)Actions: Click behavior ("focus", "report")- And more fields for icons, timeouts, app name, etc.
See Kitty Desktop Notifications for protocol details.
Liked some of my work? Buy me a coffee (or more likely a beer)
Copyright (c) 2020-present Daniel Gatis
Licensed under MIT License

