|
| 1 | +// Package headlessterm provides a headless VT220-compatible terminal emulator. |
| 2 | +// |
| 3 | +// This package emulates a terminal without any display, making it ideal for: |
| 4 | +// - Testing terminal applications without a GUI |
| 5 | +// - Building terminal multiplexers and recorders |
| 6 | +// - Creating terminal-based web applications |
| 7 | +// - Automated testing of CLI tools |
| 8 | +// - Screen scraping and automation |
| 9 | +// |
| 10 | +// # Quick Start |
| 11 | +// |
| 12 | +// Create a terminal and write ANSI sequences to it: |
| 13 | +// |
| 14 | +// term := headlessterm.New() |
| 15 | +// term.WriteString("\x1b[31mHello \x1b[32mWorld\x1b[0m!") |
| 16 | +// fmt.Println(term.String()) // "Hello World!" |
| 17 | +// |
| 18 | +// # Architecture |
| 19 | +// |
| 20 | +// The package is organized around these core types: |
| 21 | +// |
| 22 | +// - [Terminal]: The main emulator that processes ANSI sequences |
| 23 | +// - [Buffer]: A 2D grid of cells with scrollback support |
| 24 | +// - [Cell]: A single character with colors and attributes |
| 25 | +// - [Cursor]: Tracks position and rendering style |
| 26 | +// |
| 27 | +// # Terminal |
| 28 | +// |
| 29 | +// Terminal is the main entry point. It implements [io.Writer] so you can write |
| 30 | +// raw bytes containing ANSI escape sequences: |
| 31 | +// |
| 32 | +// term := headlessterm.New( |
| 33 | +// headlessterm.WithSize(24, 80), // 24 rows, 80 columns |
| 34 | +// headlessterm.WithScrollback(storage), // Enable scrollback |
| 35 | +// headlessterm.WithResponse(ptyWriter), // Handle terminal responses |
| 36 | +// ) |
| 37 | +// |
| 38 | +// // Process output from a command |
| 39 | +// cmd := exec.Command("ls", "-la", "--color") |
| 40 | +// cmd.Stdout = term |
| 41 | +// cmd.Run() |
| 42 | +// |
| 43 | +// // Read the result |
| 44 | +// for row := 0; row < term.Rows(); row++ { |
| 45 | +// fmt.Println(term.LineContent(row)) |
| 46 | +// } |
| 47 | +// |
| 48 | +// # Dual Buffers |
| 49 | +// |
| 50 | +// Terminal maintains two buffers: |
| 51 | +// |
| 52 | +// - Primary buffer: Normal mode with optional scrollback storage |
| 53 | +// - Alternate buffer: Used by full-screen apps (vim, less, htop), no scrollback |
| 54 | +// |
| 55 | +// Applications switch buffers via ANSI sequences (CSI ?1049h/l). Check which |
| 56 | +// buffer is active: |
| 57 | +// |
| 58 | +// if term.IsAlternateScreen() { |
| 59 | +// // Full-screen app is running |
| 60 | +// } |
| 61 | +// |
| 62 | +// # Cells and Attributes |
| 63 | +// |
| 64 | +// Each cell stores a character with styling information: |
| 65 | +// |
| 66 | +// cell := term.Cell(row, col) |
| 67 | +// if cell != nil { |
| 68 | +// fmt.Printf("Char: %c\n", cell.Char) |
| 69 | +// fmt.Printf("Bold: %v\n", cell.HasFlag(headlessterm.CellFlagBold)) |
| 70 | +// fmt.Printf("FG: %v\n", cell.Fg) |
| 71 | +// fmt.Printf("BG: %v\n", cell.Bg) |
| 72 | +// } |
| 73 | +// |
| 74 | +// Cell flags include: Bold, Dim, Italic, Underline, Blink, Reverse, Hidden, Strike. |
| 75 | +// |
| 76 | +// # Colors |
| 77 | +// |
| 78 | +// Colors are stored using Go's [image/color] interface. The package supports: |
| 79 | +// |
| 80 | +// - Named colors (indices 0-15 for standard ANSI colors) |
| 81 | +// - 256-color palette (indices 0-255) |
| 82 | +// - True color (24-bit RGB via [color.RGBA]) |
| 83 | +// |
| 84 | +// Use [ResolveDefaultColor] to convert any color to RGBA: |
| 85 | +// |
| 86 | +// rgba := headlessterm.ResolveDefaultColor(cell.Fg, true) |
| 87 | +// |
| 88 | +// # Scrollback |
| 89 | +// |
| 90 | +// Lines scrolled off the top of the primary buffer can be stored for later access. |
| 91 | +// Implement [ScrollbackProvider] or use the built-in memory storage: |
| 92 | +// |
| 93 | +// // In-memory scrollback with 10000 line limit |
| 94 | +// storage := headlessterm.NewMemoryScrollback(10000) |
| 95 | +// term := headlessterm.New(headlessterm.WithScrollback(storage)) |
| 96 | +// |
| 97 | +// // Access scrollback |
| 98 | +// for i := 0; i < term.ScrollbackLen(); i++ { |
| 99 | +// line := term.ScrollbackLine(i) // []Cell |
| 100 | +// } |
| 101 | +// |
| 102 | +// # Providers |
| 103 | +// |
| 104 | +// Providers handle terminal events and queries. All are optional with no-op defaults: |
| 105 | +// |
| 106 | +// - [ResponseProvider]: Writes terminal responses (cursor position, etc.) |
| 107 | +// - [BellProvider]: Handles bell/beep events |
| 108 | +// - [TitleProvider]: Handles window title changes (OSC 0/1/2) |
| 109 | +// - [ClipboardProvider]: Handles clipboard operations (OSC 52) |
| 110 | +// - [ScrollbackProvider]: Stores lines scrolled off screen |
| 111 | +// - [RecordingProvider]: Captures raw input for replay |
| 112 | +// - [SizeProvider]: Provides pixel dimensions for queries |
| 113 | +// - [ShellIntegrationProvider]: Handles shell integration marks (OSC 133) |
| 114 | +// |
| 115 | +// Example with providers: |
| 116 | +// |
| 117 | +// term := headlessterm.New( |
| 118 | +// headlessterm.WithResponse(os.Stdout), |
| 119 | +// headlessterm.WithBell(&MyBellHandler{}), |
| 120 | +// headlessterm.WithTitle(&MyTitleHandler{}), |
| 121 | +// ) |
| 122 | +// |
| 123 | +// # Middleware |
| 124 | +// |
| 125 | +// Middleware intercepts ANSI handler calls for custom behavior: |
| 126 | +// |
| 127 | +// mw := &headlessterm.Middleware{ |
| 128 | +// Input: func(r rune, next func(rune)) { |
| 129 | +// log.Printf("Input: %c", r) |
| 130 | +// next(r) // Call default handler |
| 131 | +// }, |
| 132 | +// Bell: func(next func()) { |
| 133 | +// log.Println("Bell!") |
| 134 | +// // Don't call next() to suppress the bell |
| 135 | +// }, |
| 136 | +// } |
| 137 | +// term := headlessterm.New(headlessterm.WithMiddleware(mw)) |
| 138 | +// |
| 139 | +// # Terminal Modes |
| 140 | +// |
| 141 | +// Various terminal behaviors are controlled by mode flags: |
| 142 | +// |
| 143 | +// term.HasMode(headlessterm.ModeLineWrap) // Auto line wrap enabled? |
| 144 | +// term.HasMode(headlessterm.ModeShowCursor) // Cursor visible? |
| 145 | +// term.HasMode(headlessterm.ModeBracketedPaste) // Bracketed paste enabled? |
| 146 | +// |
| 147 | +// See [TerminalMode] for all available modes. |
| 148 | +// |
| 149 | +// # Dirty Tracking |
| 150 | +// |
| 151 | +// Track which cells changed for efficient rendering: |
| 152 | +// |
| 153 | +// if term.HasDirty() { |
| 154 | +// for _, pos := range term.DirtyCells() { |
| 155 | +// // Redraw cell at pos.Row, pos.Col |
| 156 | +// } |
| 157 | +// term.ClearDirty() |
| 158 | +// } |
| 159 | +// |
| 160 | +// # Selection |
| 161 | +// |
| 162 | +// Manage text selections for copy/paste: |
| 163 | +// |
| 164 | +// term.SetSelection( |
| 165 | +// headlessterm.Position{Row: 0, Col: 0}, |
| 166 | +// headlessterm.Position{Row: 2, Col: 10}, |
| 167 | +// ) |
| 168 | +// text := term.GetSelectedText() |
| 169 | +// term.ClearSelection() |
| 170 | +// |
| 171 | +// # Search |
| 172 | +// |
| 173 | +// Find text in the visible screen or scrollback: |
| 174 | +// |
| 175 | +// matches := term.Search("error") |
| 176 | +// for _, pos := range matches { |
| 177 | +// fmt.Printf("Found at row %d, col %d\n", pos.Row, pos.Col) |
| 178 | +// } |
| 179 | +// |
| 180 | +// // Search scrollback (returns negative row numbers) |
| 181 | +// scrollbackMatches := term.SearchScrollback("error") |
| 182 | +// |
| 183 | +// # Snapshots |
| 184 | +// |
| 185 | +// Capture the terminal state for serialization or rendering: |
| 186 | +// |
| 187 | +// // Text only (smallest) |
| 188 | +// snap := term.Snapshot(headlessterm.SnapshotDetailText) |
| 189 | +// |
| 190 | +// // With style segments (good for HTML rendering) |
| 191 | +// snap := term.Snapshot(headlessterm.SnapshotDetailStyled) |
| 192 | +// |
| 193 | +// // Full cell data (complete state, includes image references) |
| 194 | +// snap := term.Snapshot(headlessterm.SnapshotDetailFull) |
| 195 | +// |
| 196 | +// // Convert to JSON |
| 197 | +// data, _ := json.Marshal(snap) |
| 198 | +// |
| 199 | +// Snapshots include detailed attribute information: |
| 200 | +// - Underline styles: "single", "double", "curly", "dotted", "dashed" |
| 201 | +// - Blink types: "slow", "fast" |
| 202 | +// - Underline color (separate from foreground) |
| 203 | +// - Cell image references with UV coordinates for texture mapping |
| 204 | +// |
| 205 | +// # Image Support |
| 206 | +// |
| 207 | +// The terminal supports inline images via Sixel and Kitty graphics protocols: |
| 208 | +// |
| 209 | +// // Check if images are enabled |
| 210 | +// if term.SixelEnabled() || term.KittyEnabled() { |
| 211 | +// // Process image sequences |
| 212 | +// } |
| 213 | +// |
| 214 | +// // Access stored images |
| 215 | +// for _, placement := range term.ImagePlacements() { |
| 216 | +// img := term.Image(placement.ImageID) |
| 217 | +// // img.Data contains RGBA pixels |
| 218 | +// } |
| 219 | +// |
| 220 | +// // Configure image memory budget |
| 221 | +// term.SetImageMaxMemory(100 * 1024 * 1024) // 100MB |
| 222 | +// |
| 223 | +// # Shell Integration |
| 224 | +// |
| 225 | +// Track shell prompts and command output (OSC 133): |
| 226 | +// |
| 227 | +// term := headlessterm.New( |
| 228 | +// headlessterm.WithShellIntegration(&MyHandler{}), |
| 229 | +// ) |
| 230 | +// |
| 231 | +// // Navigate between prompts |
| 232 | +// nextRow := term.NextPromptRow(currentRow, -1) |
| 233 | +// prevRow := term.PrevPromptRow(currentRow, -1) |
| 234 | +// |
| 235 | +// // Get last command output |
| 236 | +// output := term.GetLastCommandOutput() |
| 237 | +// |
| 238 | +// # Auto-Resize Mode |
| 239 | +// |
| 240 | +// In auto-resize mode, the buffer grows instead of scrolling: |
| 241 | +// |
| 242 | +// term := headlessterm.New(headlessterm.WithAutoResize()) |
| 243 | +// |
| 244 | +// // Capture complete output without truncation |
| 245 | +// cmd.Stdout = term |
| 246 | +// cmd.Run() |
| 247 | +// |
| 248 | +// // Buffer has grown to fit all output |
| 249 | +// fmt.Printf("Total rows: %d\n", term.Rows()) |
| 250 | +// |
| 251 | +// # Thread Safety |
| 252 | +// |
| 253 | +// All Terminal methods are safe for concurrent use. The terminal uses internal |
| 254 | +// locking to protect state. However, if you need to perform multiple operations |
| 255 | +// atomically, you should use your own synchronization. |
| 256 | +// |
| 257 | +// # Supported ANSI Sequences |
| 258 | +// |
| 259 | +// The terminal supports a comprehensive set of ANSI escape sequences including: |
| 260 | +// |
| 261 | +// - Cursor movement (CUU, CUD, CUF, CUB, CUP, HVP, etc.) |
| 262 | +// - Cursor save/restore (DECSC, DECRC) |
| 263 | +// - Erase commands (ED, EL, ECH) |
| 264 | +// - Insert/delete (ICH, DCH, IL, DL) |
| 265 | +// - Scrolling (SU, SD, DECSTBM) |
| 266 | +// - Character attributes (SGR) with full color support |
| 267 | +// - Terminal modes (DECSET, DECRST) |
| 268 | +// - Device status reports (DSR) |
| 269 | +// - Alternate screen buffer |
| 270 | +// - Bracketed paste mode |
| 271 | +// - Mouse reporting |
| 272 | +// - Window title (OSC 0/1/2) |
| 273 | +// - Clipboard (OSC 52) |
| 274 | +// - Hyperlinks (OSC 8) |
| 275 | +// - Shell integration (OSC 133) |
| 276 | +// - Sixel and Kitty graphics |
| 277 | +// |
| 278 | +// For the complete list of supported sequences, see the [go-ansicode] package |
| 279 | +// documentation. |
| 280 | +// |
| 281 | +// [go-ansicode]: https://github.com/danielgatis/go-ansicode |
| 282 | +package headlessterm |
0 commit comments