terminal

package
v0.0.34 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

terminal

Low-level terminal control with double-buffered rendering, input decoding, styling, and mouse/keyboard event handling. Provides the foundation for building rich terminal user interfaces.

Features

  • Double-buffered rendering with dirty region tracking
  • Frame-based rendering API with atomic updates
  • ANSI escape sequence generation and parsing
  • Wide character support (CJK, emojis)
  • Mouse event parsing (SGR extended mode)
  • Keyboard event decoding with modifiers
  • Raw mode and alternate screen buffer
  • Cursor control and visibility
  • RGB and 256-color support
  • OSC 8 hyperlink support
  • Terminal resize handling with callbacks
  • Performance metrics collection
  • Bracketed paste support
  • Kitty keyboard protocol detection

Usage Examples

Basic Rendering
package main

import (
    "github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
    term, _ := terminal.NewTerminal()
    defer term.Close()

    // Enable raw mode and alternate screen
    term.EnableRawMode()
    term.EnableAlternateScreen()
    term.HideCursor()

    // Begin frame (locks terminal)
    frame, _ := term.BeginFrame()

    // Draw content
    style := terminal.NewStyle().WithForeground(terminal.ColorGreen).WithBold()
    frame.PrintStyled(0, 0, "Hello, World!", style)

    // End frame (flushes to terminal)
    term.EndFrame(frame)
}
Styled Text
frame, _ := term.BeginFrame()

// Create styles
headerStyle := terminal.NewStyle().
    WithForeground(terminal.ColorBlue).
    WithBold()

warningStyle := terminal.NewStyle().
    WithForeground(terminal.ColorYellow).
    WithBgRGB(terminal.RGB{40, 40, 40})

// Print styled text
frame.PrintStyled(0, 0, "Header", headerStyle)
frame.PrintStyled(0, 1, "Warning!", warningStyle)

term.EndFrame(frame)
RGB Colors
// True color support
rgb := terminal.NewRGB(100, 200, 255)
style := terminal.NewStyle().WithFgRGB(rgb)

frame.PrintStyled(0, 0, "True color text", style)
Filling Regions
frame, _ := term.BeginFrame()

// Fill rectangle with character and style
boxStyle := terminal.NewStyle().
    WithBackground(terminal.ColorBlue)

frame.FillStyled(5, 5, 20, 10, ' ', boxStyle)

term.EndFrame(frame)
SubFrames for Layout
import "image"

frame, _ := term.BeginFrame()

// Create subframe for a panel
panelRect := image.Rect(10, 5, 50, 20)
panel := frame.SubFrame(panelRect)

// Draw in panel using (0,0) relative coordinates
panel.PrintStyled(0, 0, "Panel Header", headerStyle)
panel.PrintStyled(0, 1, "Content", textStyle)

// SubFrame automatically handles clipping and translation
term.EndFrame(frame)
Keyboard Input
import "os"

decoder := terminal.NewKeyDecoder(os.Stdin)

for {
    event, err := decoder.ReadEvent()
    if err != nil {
        break
    }

    switch e := event.(type) {
    case terminal.KeyEvent:
        if e.Key == terminal.KeyCtrlC {
            return
        }
        if e.Rune != 0 {
            fmt.Printf("Char: %c\n", e.Rune)
        }
        if e.Paste != "" {
            fmt.Printf("Pasted: %s\n", e.Paste)
        }
    }
}
Mouse Input
term.EnableMouseTracking()
defer term.DisableMouseTracking()

decoder := terminal.NewKeyDecoder(os.Stdin)

for {
    event, err := decoder.ReadEvent()
    if err != nil {
        break
    }

    switch e := event.(type) {
    case terminal.MouseEvent:
        if e.Type == terminal.MousePress && e.Button == terminal.MouseButtonLeft {
            fmt.Printf("Clicked at (%d, %d)\n", e.X, e.Y)
        }
        if e.Type == terminal.MouseScroll {
            fmt.Printf("Scrolled: %d\n", e.DeltaY)
        }
    }
}
// OSC 8 hyperlink (works in iTerm2, WezTerm, kitty)
link := terminal.Hyperlink{
    Text: "Click here",
    URL:  "https://example.com",
    Style: terminal.NewStyle().
        WithForeground(terminal.ColorBlue).
        WithUnderline(),
}

frame.PrintHyperlink(5, 5, link)

// Fallback format: "Text (URL)"
frame.PrintHyperlinkFallback(5, 6, link)
Terminal Resize Handling
term.WatchResize()
defer term.StopWatchResize()

unregister := term.OnResize(func(width, height int) {
    log.Printf("Terminal resized to %dx%d\n", width, height)
    // Trigger re-render
    render()
})
defer unregister()
Performance Metrics
term.EnableMetrics()

// ... render frames ...

metrics := term.GetMetrics()
fmt.Printf("Frames: %d\n", metrics.FrameCount)
fmt.Printf("Avg render time: %v\n", metrics.AvgRenderTime)
fmt.Printf("Cells updated: %d\n", metrics.TotalCellsUpdated)
Enhanced Keyboard Protocol
// Detect Kitty keyboard protocol support
if term.DetectKittyProtocol() {
    term.EnableEnhancedKeyboard()
    defer term.DisableEnhancedKeyboard()
}

// Now Shift+Enter, Ctrl+Enter, etc. are distinguishable
decoder := terminal.NewKeyDecoder(os.Stdin)
for {
    event, err := decoder.ReadEvent()
    if err != nil {
        break
    }

    if key, ok := event.(terminal.KeyEvent); ok {
        if key.Key == terminal.KeyEnter && key.Shift {
            fmt.Println("Shift+Enter pressed")
        }
    }
}

API Reference

Terminal Management
Function Description Inputs Outputs
NewTerminal Create new terminal instance None *Terminal, error
NewTestTerminal Create terminal for testing width, height int, out io.Writer *Terminal
Close Clean up terminal state None error
Size Get terminal dimensions None width, height int
RefreshSize Update cached terminal size None error
Frame Rendering
Method Description Inputs Outputs
BeginFrame Start frame rendering (locks terminal) None RenderFrame, error
EndFrame Finish frame and flush changes frame RenderFrame error
Flush Manually flush buffer changes None None
RenderFrame Methods
Method Description Inputs Outputs
SetCell Set character and style at position x, y int, char rune, style Style error
PrintStyled Print text with wrapping x, y int, text string, style Style error
PrintTruncated Print text with truncation x, y int, text string, style Style error
FillStyled Fill rectangle with character x, y, width, height int, char rune, style Style error
Fill Fill entire frame char rune, style Style error
Size Get frame dimensions None width, height int
GetBounds Get frame bounds None image.Rectangle
SubFrame Create subframe for region rect image.Rectangle RenderFrame
PrintHyperlink Print clickable hyperlink (OSC 8) x, y int, link Hyperlink error
Input Decoding
Function Description Inputs Outputs
NewKeyDecoder Create input decoder r io.Reader *KeyDecoder
ReadEvent Read next input event None Event, error
ParseMouseEvent Parse mouse event from bytes seq []byte *MouseEvent, error
Styles
Function Description Inputs Outputs
NewStyle Create new style None Style
NewRGB Create RGB color r, g, b uint8 RGB

See full API reference in the package godoc.

  • tui - Declarative TUI library built on terminal
  • termtest - Testing utilities for terminal output
  • termsession - Terminal session recording/playback
  • color - Color manipulation and gradients

Design Notes

The terminal package uses double-buffering to minimize flicker and optimize rendering. Only cells that changed since the last frame are redrawn. The dirty region tracking further optimizes by skipping unchanged screen areas entirely.

Frame-based rendering ensures atomic updates: either the entire frame renders successfully, or nothing changes. This prevents partial renders that can occur with immediate-mode APIs.

SubFrames provide coordinate translation and clipping automatically. When drawing to a SubFrame, always use coordinates relative to (0,0), not the bounds returned by GetBounds(). The SubFrame handles translation internally.

Documentation

Overview

Package terminal provides OSC 8 hyperlink support for terminals.

OSC 8 is a terminal escape sequence that enables clickable hyperlinks. It is supported by many modern terminals including:

  • iTerm2
  • WezTerm
  • kitty
  • Hyper
  • Windows Terminal
  • GNOME Terminal (3.26+)
  • Konsole (18.08+)

For unsupported terminals, the escape codes are simply ignored, so it's safe to use unconditionally.

Example usage:

// Simple hyperlink
fmt.Print(terminal.Format("https://example.com", "Click here"))

// Hyperlink with custom ID (for grouping)
fmt.Print(terminal.FormatWithID("https://example.com", "Click here", "link1"))

// Check if URL is valid first
if err := terminal.ValidateURL("https://example.com"); err != nil {
    log.Printf("Invalid URL: %v", err)
}

Package terminal provides low-level terminal control, input decoding, styling, and rendering.

This package enables building rich terminal user interfaces with features like:

  • Double-buffered rendering with dirty region tracking for flicker-free updates
  • Frame-based rendering API (BeginFrame/EndFrame) for atomic updates
  • Keyboard input decoding supporting multi-byte UTF-8, ANSI escape sequences, and modifiers
  • Mouse event handling with click, drag, hover, and scroll support
  • Rich text styling with RGB colors, bold, italic, underline, and more
  • Hyperlinks using OSC 8 protocol for clickable terminal links
  • Raw mode and alternate screen buffer management
  • Terminal resize detection and callbacks
  • Performance metrics for monitoring rendering efficiency

Basic Rendering

The recommended way to render content is using the frame-based API:

term, err := terminal.NewTerminal()
if err != nil {
    log.Fatal(err)
}
defer term.Close()

// Enable alternate screen and raw mode for full-screen apps
term.EnableAlternateScreen()
term.EnableRawMode()

// Render a frame
frame, _ := term.BeginFrame()
style := terminal.NewStyle().WithForeground(terminal.ColorBlue).WithBold()
frame.PrintStyled(0, 0, "Hello, World!", style)
term.EndFrame(frame)

Input Handling

The KeyDecoder decodes terminal input into structured events:

decoder := terminal.NewKeyDecoder(os.Stdin)
for {
    event, err := decoder.ReadEvent()
    if err != nil {
        break
    }
    switch e := event.(type) {
    case terminal.KeyEvent:
        if e.Key == terminal.KeyCtrlC {
            return
        }
        fmt.Printf("Key: %c\n", e.Rune)
    case terminal.MouseEvent:
        fmt.Printf("Mouse: %d,%d\n", e.X, e.Y)
    }
}

Styling

Text can be styled using the Style type which supports colors, attributes, and hyperlinks:

// Basic colors
style := terminal.NewStyle().
    WithForeground(terminal.ColorRed).
    WithBackground(terminal.ColorWhite)

// RGB colors
rgb := terminal.NewRGB(100, 150, 200)
style = style.WithFgRGB(rgb)

// Text attributes
style = style.WithBold().WithItalic().WithUnderline()

// Apply to text
styledText := style.Apply("Hello")

Thread Safety

Most Terminal methods are thread-safe, including Size, Clear, MoveCursor, SetCell, and the BeginFrame/EndFrame rendering pattern. However, some methods that emit raw escape sequences (HideCursor, ShowCursor, EnableAlternateScreen, DisableAlternateScreen, EnableMouseTracking, DisableMouseTracking, etc.) do not acquire locks to avoid overhead, as they're typically called during setup/teardown. For rendering, use the BeginFrame/EndFrame pattern which locks the terminal for exclusive access during frame composition.

Performance

The terminal uses double-buffering and dirty region tracking to minimize the amount of data written to the terminal. Only changed cells are updated on each frame. Enable metrics to monitor rendering performance:

term.EnableMetrics()
// ... render frames ...
snapshot := term.GetMetrics()
fmt.Println(snapshot.String())
Example

Example demonstrates basic terminal rendering with the frame-based API.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	// Create a test terminal (in real code, use terminal.NewTerminal())
	var output strings.Builder
	term := terminal.NewTestTerminal(40, 10, &output)

	// Render a frame
	frame, _ := term.BeginFrame()
	style := terminal.NewStyle().WithForeground(terminal.ColorBlue).WithBold()
	frame.PrintStyled(0, 0, "Hello, World!", style)
	term.EndFrame(frame)

	// The output contains ANSI escape codes for positioning and styling
	fmt.Println("Frame rendered successfully")
}
Output:
Frame rendered successfully

Index

Examples

Constants

View Source
const (
	ColorDefault       = color.NoColor
	ColorBlack         = color.Black
	ColorRed           = color.Red
	ColorGreen         = color.Green
	ColorYellow        = color.Yellow
	ColorBlue          = color.Blue
	ColorMagenta       = color.Magenta
	ColorCyan          = color.Cyan
	ColorWhite         = color.White
	ColorBrightBlack   = color.BrightBlack
	ColorBrightRed     = color.BrightRed
	ColorBrightGreen   = color.BrightGreen
	ColorBrightYellow  = color.BrightYellow
	ColorBrightBlue    = color.BrightBlue
	ColorBrightMagenta = color.BrightMagenta
	ColorBrightCyan    = color.BrightCyan
	ColorBrightWhite   = color.BrightWhite
)

Re-export color constants for backward compatibility

Variables

View Source
var (
	// SingleBorder uses single-line box drawing characters
	SingleBorder = BorderStyle{
		TopLeft:     "┌",
		TopRight:    "┐",
		BottomLeft:  "└",
		BottomRight: "┘",
		Horizontal:  "─",
		Vertical:    "│",
		Cross:       "┼",
		TopJoin:     "┬",
		BottomJoin:  "┴",
		LeftJoin:    "├",
		RightJoin:   "┤",
	}

	// DoubleBorder uses double-line box drawing characters
	DoubleBorder = BorderStyle{
		TopLeft:     "╔",
		TopRight:    "╗",
		BottomLeft:  "╚",
		BottomRight: "╝",
		Horizontal:  "═",
		Vertical:    "║",
		Cross:       "╬",
		TopJoin:     "╦",
		BottomJoin:  "╩",
		LeftJoin:    "╠",
		RightJoin:   "╣",
	}

	// RoundedBorder uses rounded corners
	RoundedBorder = BorderStyle{
		TopLeft:     "╭",
		TopRight:    "╮",
		BottomLeft:  "╰",
		BottomRight: "╯",
		Horizontal:  "─",
		Vertical:    "│",
		Cross:       "┼",
		TopJoin:     "┬",
		BottomJoin:  "┴",
		LeftJoin:    "├",
		RightJoin:   "┤",
	}

	// ThickBorder uses thick box drawing characters
	ThickBorder = BorderStyle{
		TopLeft:     "┏",
		TopRight:    "┓",
		BottomLeft:  "┗",
		BottomRight: "┛",
		Horizontal:  "━",
		Vertical:    "┃",
		Cross:       "╋",
		TopJoin:     "┳",
		BottomJoin:  "┻",
		LeftJoin:    "┣",
		RightJoin:   "┫",
	}

	// ASCIIBorder uses ASCII characters for compatibility
	ASCIIBorder = BorderStyle{
		TopLeft:     "+",
		TopRight:    "+",
		BottomLeft:  "+",
		BottomRight: "+",
		Horizontal:  "-",
		Vertical:    "|",
		Cross:       "+",
		TopJoin:     "+",
		BottomJoin:  "+",
		LeftJoin:    "+",
		RightJoin:   "+",
	}
)

Predefined border styles

View Source
var (
	NewRGB          = color.NewRGB
	Gradient        = color.Gradient
	RainbowGradient = color.RainbowGradient
	SmoothRainbow   = color.SmoothRainbow
	MultiGradient   = color.MultiGradient
)

Re-export color functions for backward compatibility

View Source
var (
	ErrOutOfBounds   = errors.New("coordinates out of bounds")
	ErrNotInRawMode  = errors.New("operation requires raw mode")
	ErrClosed        = errors.New("terminal is closed")
	ErrInvalidFrame  = errors.New("invalid frame passed to EndFrame")
	ErrAlreadyActive = errors.New("component is already active")
)

Common errors

Functions

func End

func End() string

End returns the OSC 8 escape sequence to end a hyperlink. Format: ESC ] 8 ; ; ST

func Fallback

func Fallback(targetURL, text string) string

Fallback returns a fallback representation for terminals that don't support OSC 8. Format: "text (URL)" For example: "Click here (https://example.com)"

func Format

func Format(targetURL, text string) string

Format returns a complete hyperlink with the given URL and display text. This wraps the text in OSC 8 start and end sequences.

func FormatWithID

func FormatWithID(targetURL, text, id string) string

FormatWithID returns a complete hyperlink with a custom ID. The ID can be used to group multiple hyperlink segments as a single link.

func OSC8End

func OSC8End() string

OSC8End returns the OSC 8 escape sequence to end a hyperlink. Format: \033]8;;\033\\

func OSC8Start

func OSC8Start(url string) string

OSC8Start returns the OSC 8 escape sequence to start a hyperlink. Format: \033]8;;URL\033\\

func RedactCredentials

func RedactCredentials(input string) string

RedactCredentials can be called manually to test redaction

func Start

func Start(targetURL string) string

Start returns the OSC 8 escape sequence to start a hyperlink. Format: ESC ] 8 ; ; URL ST Where ST (String Terminator) is ESC \

func StartWithID

func StartWithID(targetURL, id string) string

StartWithID returns the OSC 8 escape sequence to start a hyperlink with an ID. The ID can be used to group multiple hyperlink segments together. Format: ESC ] 8 ; id=ID ; URL ST

func StripOSC8

func StripOSC8(text string) string

StripOSC8 removes OSC 8 hyperlink escape sequences from text. This is useful for getting plain text from hyperlink-formatted strings.

func ValidateAbsoluteURL

func ValidateAbsoluteURL(targetURL string) error

ValidateAbsoluteURL checks if a URL is a valid absolute URL with a scheme. Use this for stricter validation when you want to ensure the URL is absolute.

func ValidateURL

func ValidateURL(targetURL string) error

ValidateURL checks if a URL is valid for use in a hyperlink. Returns nil if valid, or an error describing the issue. Note: This is lenient and accepts relative URLs and URLs without schemes.

Types

type Alignment

type Alignment int

Alignment represents text alignment

const (
	AlignLeft Alignment = iota
	AlignCenter
	AlignRight
)

type BorderStyle

type BorderStyle struct {
	TopLeft     string
	TopRight    string
	BottomLeft  string
	BottomRight string
	Horizontal  string
	Vertical    string
	Cross       string
	TopJoin     string
	BottomJoin  string
	LeftJoin    string
	RightJoin   string
}

BorderStyle defines the characters used for drawing borders

type Box

type Box struct {
	Content     []string
	Border      BorderStyle
	BorderStyle Style
	Padding     int
}

Box is a simpler API for drawing boxes

func NewBox

func NewBox(content []string) *Box

NewBox creates a new box with content

func (*Box) Draw

func (b *Box) Draw(t RenderFrame, x, y int)

Draw renders the box at the specified position

type Cell

type Cell struct {
	Char         rune
	Trailing     string // remaining runes of a multi-rune grapheme cluster; usually empty
	Style        Style
	Width        int  // Display width of the cluster (0 for continuation cells, 1-2 for actual chars)
	Continuation bool // True if this cell is a continuation of a wide character
}

Cell represents a single character cell on the terminal.

For simple ASCII or BMP characters, Char holds the entire cell content and Trailing is empty. For multi-rune grapheme clusters (VS16 emoji, keycaps, ZWJ sequences, regional indicator flag pairs, skin-toned emoji, combining marks), Char is the first rune of the cluster and Trailing holds the remaining bytes of the cluster so they can be emitted together on render. Width is the display width of the whole cluster.

type Color

type Color = color.Color

Re-export color types for backward compatibility

type CursorStyle

type CursorStyle int

CursorStyle represents a visual cursor style hint for mouse regions. These are semantic hints - actual terminal cursor changes must be implemented separately.

const (
	CursorDefault    CursorStyle = iota // Default arrow cursor
	CursorPointer                       // Pointing hand (for clickable elements)
	CursorText                          // I-beam text cursor (for text input)
	CursorResizeEW                      // East-West resize cursor (horizontal)
	CursorResizeNS                      // North-South resize cursor (vertical)
	CursorResizeNESW                    // Northeast-Southwest diagonal resize
	CursorResizeNWSE                    // Northwest-Southeast diagonal resize
	CursorMove                          // Move cursor (for draggable elements)
	CursorNotAllowed                    // Not allowed cursor (for disabled elements)
)

type DirtyRegion

type DirtyRegion struct {
	MinX int
	MinY int
	MaxX int
	MaxY int
	// contains filtered or unexported fields
}

DirtyRegion tracks the rectangular area that has been modified

func (*DirtyRegion) Clear

func (dr *DirtyRegion) Clear()

Clear resets the dirty region

func (*DirtyRegion) Empty

func (dr *DirtyRegion) Empty() bool

Empty returns true if the dirty region is empty

func (*DirtyRegion) Mark

func (dr *DirtyRegion) Mark(x, y int)

Mark marks a cell as dirty, expanding the dirty region if necessary

func (*DirtyRegion) MarkRect

func (dr *DirtyRegion) MarkRect(x, y, width, height int)

MarkRect marks a rectangular region as dirty in O(1) time using bounding box expansion

type Event

type Event interface {
	// Timestamp returns when the event occurred.
	Timestamp() time.Time
}

Event represents any input event from the terminal. Events can be keyboard input (KeyEvent) or mouse input (MouseEvent). All events provide a timestamp indicating when they occurred.

Use type assertion to determine the specific event type:

event, err := decoder.ReadEvent()
switch e := event.(type) {
case terminal.KeyEvent:
    // Handle keyboard input
    if e.Key == terminal.KeyEnter {
        fmt.Println("Enter pressed")
    }
case terminal.MouseEvent:
    // Handle mouse input
    if e.Type == terminal.MouseClick {
        fmt.Printf("Clicked at %d,%d\n", e.X, e.Y)
    }
}

type Frame

type Frame struct {
	X           int
	Y           int
	Width       int
	Height      int
	Border      BorderStyle
	BorderStyle Style
	Title       string
	TitleStyle  Style
	TitleAlign  Alignment
}

Frame represents a bordered frame

func NewFrame

func NewFrame(x, y, width, height int) *Frame

NewFrame creates a new frame with default border

func (*Frame) Clear

func (f *Frame) Clear(t RenderFrame)

Clear clears the content inside the frame (not the border)

func (*Frame) Draw

func (f *Frame) Draw(t RenderFrame)

Draw renders the frame to the terminal Updated to use RenderFrame

func (*Frame) WithBorderStyle

func (f *Frame) WithBorderStyle(style BorderStyle) *Frame

WithBorderStyle sets the border style

func (*Frame) WithColor

func (f *Frame) WithColor(style Style) *Frame

WithColor sets the border color

func (*Frame) WithTitle

func (f *Frame) WithTitle(title string, align Alignment) *Frame

WithTitle sets the frame title

func (*Frame) WithTitleStyle

func (f *Frame) WithTitleStyle(style Style) *Frame

WithTitleStyle sets the title style

type Hyperlink struct {
	URL   string // The target URL (e.g., "https://example.com")
	Text  string // The display text (e.g., "Click here")
	Style Style  // Styling for the link text (default: blue, underlined)
}

Hyperlink represents a clickable hyperlink in the terminal using the OSC 8 protocol.

OSC 8 is a terminal escape sequence standard that makes text clickable. When clicked, the terminal opens the URL in the default browser.

OSC 8 is supported by many modern terminals:

  • iTerm2 (macOS)
  • WezTerm (cross-platform)
  • kitty (Linux, macOS)
  • Windows Terminal
  • GNOME Terminal 3.26+
  • Konsole 18.08+
  • Hyper

For unsupported terminals, the escape codes are ignored and only the text is shown.

Create hyperlinks and render them using RenderFrame:

link := terminal.NewHyperlink("https://example.com", "Click here")
frame.PrintHyperlink(10, 5, link)

Or use the fallback format for terminals without OSC 8 support:

frame.PrintHyperlinkFallback(10, 5, link) // Prints: Click here (https://example.com)

Hyperlinks have a default style (blue, underlined) but can be customized:

link := terminal.NewHyperlink("https://example.com", "Click here")
link = link.WithStyle(terminal.NewStyle().WithForeground(terminal.ColorGreen))
func NewHyperlink(url, text string) Hyperlink

NewHyperlink creates a new Hyperlink with the given URL and display text. The hyperlink is given a default style (blue foreground, underlined).

Example:

link := terminal.NewHyperlink("https://golang.org", "Go Website")
frame.PrintHyperlink(x, y, link)

func (Hyperlink) Format

func (h Hyperlink) Format() string

Format returns the formatted hyperlink with OSC 8 escape codes. If the terminal supports OSC 8, it will be clickable. The text is styled according to the hyperlink's Style field.

func (Hyperlink) FormatFallback

func (h Hyperlink) FormatFallback() string

FormatFallback returns a fallback representation for terminals that don't support OSC 8. Format: "Text (URL)" For example: "Click here (https://example.com)"

Example

ExampleHyperlink_FormatFallback demonstrates the fallback format for hyperlinks.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	link := terminal.NewHyperlink("https://example.com", "Example")

	// Fallback format shows URL in parentheses
	fallback := link.FormatFallback()

	fmt.Println(strings.Contains(fallback, "Example"))
	fmt.Println(strings.Contains(fallback, "https://example.com"))
}
Output:
true
true

func (Hyperlink) FormatWithOption

func (h Hyperlink) FormatWithOption(useOSC8 bool) string

FormatWithOption returns either the OSC 8 formatted hyperlink or the fallback, depending on the useOSC8 parameter.

func (Hyperlink) Validate

func (h Hyperlink) Validate() error

Validate checks if the hyperlink has valid components. Returns an error if the URL is invalid or empty, or if the text is empty.

func (Hyperlink) WithStyle

func (h Hyperlink) WithStyle(style Style) Hyperlink

WithStyle sets the style for the hyperlink text. By default, hyperlinks are blue and underlined.

type Key

type Key int

Key represents special keyboard keys that don't correspond to printable characters. These are commonly used control keys, function keys, and navigation keys.

When a KeyEvent has a non-zero Key value, it represents a special key press. When Key is KeyUnknown (zero), check the Rune field for printable characters.

const (
	// KeyUnknown is the zero value, used when no special key is pressed.
	// When Key is KeyUnknown, check the KeyEvent.Rune field for the actual character.
	//
	// IMPORTANT: Must be 0 so regular characters (with Key field unset) don't match special keys.
	KeyUnknown Key = 0

	// Special keys start at 1 to avoid conflicting with zero value
	KeyEnter Key = iota
	KeyTab
	KeyBackspace
	KeyEscape
	KeyArrowUp
	KeyArrowDown
	KeyArrowLeft
	KeyArrowRight
	KeyHome
	KeyEnd
	KeyPageUp
	KeyPageDown
	KeyDelete
	KeyInsert
	KeyF1
	KeyF2
	KeyF3
	KeyF4
	KeyF5
	KeyF6
	KeyF7
	KeyF8
	KeyF9
	KeyF10
	KeyF11
	KeyF12
	KeyCtrlA
	KeyCtrlB
	KeyCtrlC
	KeyCtrlD
	KeyCtrlE
	KeyCtrlF
	KeyCtrlG
	KeyCtrlH
	KeyCtrlI
	KeyCtrlJ
	KeyCtrlK
	KeyCtrlL
	KeyCtrlM
	KeyCtrlN
	KeyCtrlO
	KeyCtrlP
	KeyCtrlQ
	KeyCtrlR
	KeyCtrlS
	KeyCtrlT
	KeyCtrlU
	KeyCtrlV
	KeyCtrlW
	KeyCtrlX
	KeyCtrlY
	KeyCtrlZ
)

type KeyDecoder

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

KeyDecoder handles low-level decoding of terminal input into structured events.

The decoder reads raw bytes from an input stream (typically os.Stdin) and decodes them into KeyEvent and MouseEvent structs. It handles the complexity of multi-byte sequences, escape codes, and various terminal protocols.

Supported Input

The decoder supports:

  • Single-byte printable ASCII characters
  • Multi-byte UTF-8 characters (Unicode)
  • ANSI escape sequences (arrows, function keys, Home/End, etc.)
  • Ctrl combinations (Ctrl+A through Ctrl+Z)
  • Alt/Meta modifiers (Alt+key)
  • Shift modifiers (when supported by the terminal)
  • Mouse events in SGR extended format
  • Bracketed paste mode (large clipboard pastes)
  • Kitty keyboard protocol for enhanced key detection

Usage

Create a decoder and call ReadEvent in a loop:

decoder := terminal.NewKeyDecoder(os.Stdin)
for {
    event, err := decoder.ReadEvent()
    if err != nil {
        if err == io.EOF {
            break
        }
        log.Printf("Read error: %v", err)
        continue
    }

    switch e := event.(type) {
    case terminal.KeyEvent:
        // Handle keyboard input
    case terminal.MouseEvent:
        // Handle mouse input
    }
}

Buffering

The decoder uses internal buffering to avoid consuming more bytes than necessary. This ensures that each ReadEvent call reads exactly one complete event, making it safe to interleave with other input operations if needed.

Thread Safety

KeyDecoder is NOT thread-safe. Only one goroutine should call ReadEvent at a time.

Example

ExampleKeyDecoder demonstrates decoding keyboard input.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	// Create a decoder from a test input
	input := strings.NewReader("a\x1b[A") // 'a' key followed by up arrow
	decoder := terminal.NewKeyDecoder(input)

	// Read the first event (character 'a')
	event1, _ := decoder.ReadEvent()
	if keyEvent, ok := event1.(terminal.KeyEvent); ok {
		fmt.Printf("Character: %c\n", keyEvent.Rune)
	}

	// Read the second event (up arrow)
	event2, _ := decoder.ReadEvent()
	if keyEvent, ok := event2.(terminal.KeyEvent); ok {
		fmt.Printf("Special key: %v\n", keyEvent.Key == terminal.KeyArrowUp)
	}

}
Output:
Character: a
Special key: true

func NewKeyDecoder

func NewKeyDecoder(reader io.Reader) *KeyDecoder

NewKeyDecoder creates a new KeyDecoder that reads from the given input stream.

For production use, pass os.Stdin:

decoder := terminal.NewKeyDecoder(os.Stdin)

For testing, pass a bytes.Buffer or strings.Reader:

input := strings.NewReader("\x1b[A") // Up arrow
decoder := terminal.NewKeyDecoder(input)

The decoder must be paired with a terminal in raw mode to receive input character-by-character rather than line-by-line.

func (*KeyDecoder) ReadEvent

func (kd *KeyDecoder) ReadEvent() (Event, error)

ReadEvent reads a single input event from the input stream. It returns either a KeyEvent or MouseEvent, both implementing the Event interface. This is the recommended method for reading input when mouse support is needed.

func (*KeyDecoder) ReadKeyEvent

func (kd *KeyDecoder) ReadKeyEvent() (KeyEvent, error)

ReadKeyEvent reads a single key event from the input stream. It blocks until a complete key sequence is available.

Returns:

  • KeyEvent with either a special Key or a Rune set
  • error if read fails (io.EOF, closed pipe, etc.)

The function handles:

  • Single-byte special keys (Enter, Tab, Backspace, Ctrl+Letter)
  • Multi-byte escape sequences (arrows, function keys, Home/End, etc.)
  • UTF-8 multi-byte characters
  • Alt modifier (ESC followed by character)

func (*KeyDecoder) SetPasteTabWidth

func (kd *KeyDecoder) SetPasteTabWidth(width int)

SetPasteTabWidth configures how tabs in pasted content are handled. If width is 0 (default), tabs are preserved as-is. If width > 0, each tab is converted to that many spaces.

type KeyEvent

type KeyEvent struct {
	Key   Key       // Special key (KeyEnter, KeyArrowUp, etc.), or KeyUnknown for regular chars
	Rune  rune      // The character for printable keys (only valid when Key is KeyUnknown)
	Alt   bool      // True if Alt/Option modifier was held
	Ctrl  bool      // True if Ctrl/Command modifier was held
	Shift bool      // True if Shift modifier was held
	Paste string    // If non-empty, this event represents a paste operation (bracketed paste mode)
	Time  time.Time // When the event occurred
}

KeyEvent represents a keyboard input event from the terminal.

A KeyEvent can represent either:

  1. A special key press (arrows, function keys, etc.) - check the Key field
  2. A printable character - check the Rune field
  3. A paste operation - check the Paste field

Special Keys

When Key is not KeyUnknown, the event represents a special key like Enter, Escape, or function keys. The Alt, Ctrl, and Shift fields indicate which modifiers were held.

if event.Key == terminal.KeyEnter && event.Ctrl {
    // Ctrl+Enter pressed
}

Printable Characters

When Key is KeyUnknown, check the Rune field for the actual character:

if event.Key == terminal.KeyUnknown && event.Rune == 'a' {
    // User typed 'a'
}

Paste Events

When bracketed paste mode is enabled (via Terminal.EnableBracketedPaste), pasted content is delivered as a single KeyEvent with the Paste field set:

if event.Paste != "" {
    // User pasted text
    fmt.Println("Pasted:", event.Paste)
}

func (KeyEvent) Timestamp

func (e KeyEvent) Timestamp() time.Time

Timestamp implements the Event interface

type MetricsSnapshot

type MetricsSnapshot struct {
	TotalFrames      uint64
	CellsUpdated     uint64
	ANSICodesEmitted uint64
	BytesWritten     uint64
	SkippedFrames    uint64
	TotalRenderTime  time.Duration
	LastFrameTime    time.Duration
	MinFrameTime     time.Duration
	MaxFrameTime     time.Duration
	TotalDirtyArea   uint64
	LastDirtyArea    int
	MaxDirtyArea     int
	AvgCellsPerFrame float64
	AvgTimePerFrame  time.Duration
	AvgDirtyArea     float64
}

MetricsSnapshot represents a point-in-time snapshot of rendering metrics

func (*MetricsSnapshot) Compact

func (s *MetricsSnapshot) Compact() string

Compact returns a compact single-line representation of key metrics

func (*MetricsSnapshot) Efficiency

func (s *MetricsSnapshot) Efficiency() float64

Efficiency calculates the skip efficiency from the snapshot

func (*MetricsSnapshot) FPS

func (s *MetricsSnapshot) FPS() float64

FPS calculates frames per second from the snapshot

func (*MetricsSnapshot) String

func (s *MetricsSnapshot) String() string

String returns a formatted string representation of the metrics

type MouseButton

type MouseButton int

MouseButton represents which mouse button was involved in the event.

const (
	MouseButtonLeft   MouseButton = iota // Left mouse button
	MouseButtonMiddle                    // Middle mouse button (often scroll wheel click)
	MouseButtonRight                     // Right mouse button
	MouseButtonNone                      // No button (for move/release events)

	// Wheel buttons represent scroll wheel events
	MouseButtonWheelUp    // Mouse wheel scrolled up
	MouseButtonWheelDown  // Mouse wheel scrolled down
	MouseButtonWheelLeft  // Mouse wheel scrolled left (horizontal scroll)
	MouseButtonWheelRight // Mouse wheel scrolled right (horizontal scroll)
)

type MouseEvent

type MouseEvent struct {
	X, Y       int            // Position in terminal cells (0-based)
	Button     MouseButton    // Which button was involved
	Type       MouseEventType // Type of mouse event
	Modifiers  MouseModifiers // Keyboard modifiers held during event
	DeltaX     int            // Horizontal scroll delta (for wheel events, negative=left, positive=right)
	DeltaY     int            // Vertical scroll delta (for wheel events, negative=up, positive=down)
	Time       time.Time      // When the event occurred
	ClickCount int            // Click count: 1=single, 2=double, 3=triple
}

MouseEvent represents a mouse interaction event from the terminal.

Mouse events are generated when mouse tracking is enabled via Terminal.EnableMouseTracking() or Terminal.EnableMouseButtons().

The event includes:

  • Position (X, Y) in terminal character cells (0-based)
  • Button that was pressed/released
  • Type of event (press, release, click, drag, move, scroll)
  • Keyboard modifiers held during the event (Shift, Alt, Ctrl)
  • For wheel events: delta in X or Y direction
  • For click events: click count (1=single, 2=double, 3=triple)

Example handling different mouse events:

switch event.Type {
case terminal.MouseClick:
    if event.Button == terminal.MouseButtonLeft {
        fmt.Printf("Left click at %d,%d\n", event.X, event.Y)
    }
case terminal.MouseScroll:
    if event.Button == terminal.MouseButtonWheelUp {
        fmt.Println("Scrolled up")
    }
case terminal.MouseDrag:
    fmt.Printf("Dragging to %d,%d\n", event.X, event.Y)
}

func ParseMouseEvent

func ParseMouseEvent(seq []byte) (*MouseEvent, error)

ParseMouseEvent parses a mouse event from terminal input. It supports both SGR format (<button>;x;y[Mm]) and legacy format (3 bytes).

Example

ExampleParseMouseEvent demonstrates parsing mouse events.

package main

import (
	"fmt"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	// SGR format mouse event: left button press at (10, 5)
	// Format: ESC [ < button ; x ; y M
	sequence := []byte("<0;11;6M") // Note: coordinates are 1-based in the protocol

	event, err := terminal.ParseMouseEvent(sequence)
	if err != nil {
		fmt.Println("Parse error")
		return
	}

	fmt.Printf("Button: Left\n")
	fmt.Printf("Position: %d,%d\n", event.X, event.Y)
	fmt.Printf("Type: Press\n")

}
Output:
Button: Left
Position: 10,5
Type: Press

func (MouseEvent) Timestamp

func (e MouseEvent) Timestamp() time.Time

Timestamp implements the Event interface

type MouseEventType

type MouseEventType int

MouseEventType represents the type of mouse event. Different event types are generated based on the mouse action and state.

const (
	MousePress       MouseEventType = iota // Button pressed down
	MouseRelease                           // Button released
	MouseClick                             // Single click (press + release without significant movement)
	MouseDoubleClick                       // Double click (two clicks in quick succession)
	MouseTripleClick                       // Triple click (three clicks in quick succession)
	MouseDrag                              // Mouse moved while button held (after drag threshold exceeded)
	MouseDragStart                         // Drag operation started (threshold exceeded)
	MouseDragEnd                           // Drag operation ended (button released after drag)
	MouseDragCancel                        // Drag operation cancelled (e.g., by pressing Escape)
	MouseMove                              // Mouse moved without button pressed (requires EnableMouseTracking)
	MouseEnter                             // Mouse entered a region (generated by MouseHandler)
	MouseLeave                             // Mouse left a region (generated by MouseHandler)
	MouseScroll                            // Mouse wheel scrolled
)

type MouseHandler

type MouseHandler struct {

	// Configuration
	DoubleClickThreshold time.Duration // Maximum time between clicks for double-click
	TripleClickThreshold time.Duration // Maximum time between clicks for triple-click
	ClickMoveThreshold   int           // Maximum pixel movement to still count as click
	DragStartThreshold   int           // Minimum pixel movement to start drag
	// contains filtered or unexported fields
}

MouseHandler manages mouse regions and dispatches events to them.

MouseHandler provides a higher-level abstraction over raw mouse events, implementing:

  • Region-based event routing (only regions under the cursor receive events)
  • Click detection (press + release without movement)
  • Double-click and triple-click detection with configurable thresholds
  • Drag detection with configurable movement threshold
  • Enter/leave events when mouse moves between regions
  • Z-index based layering for overlapping regions
  • Event capture during drag operations

Basic Usage

handler := terminal.NewMouseHandler()

// Add a clickable region
region := &terminal.MouseRegion{
    X: 10, Y: 5,
    Width: 15, Height: 1,
    OnClick: func(e *terminal.MouseEvent) {
        fmt.Println("Button clicked!")
    },
}
handler.AddRegion(region)

// Route mouse events to regions
event := &terminal.MouseEvent{...}
handler.HandleEvent(event)

Event Flow

When a mouse event is received:

  1. The handler finds the topmost region (highest ZIndex) under the cursor
  2. If the region changed, OnLeave is called on the old region and OnEnter on the new
  3. The event is dispatched to the appropriate handler (OnClick, OnDrag, etc.)
  4. Click detection tracks press/release to synthesize click events
  5. Drag detection starts when movement exceeds DragStartThreshold

Configuration

The handler has configurable thresholds:

  • DoubleClickThreshold: max time between clicks for double-click (default 500ms)
  • TripleClickThreshold: max time between clicks for triple-click (default 500ms)
  • ClickMoveThreshold: max pixel movement to count as click (default 2)
  • DragStartThreshold: min pixel movement to start drag (default 5)
Example

ExampleMouseHandler demonstrates managing clickable regions.

package main

import (
	"fmt"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	handler := terminal.NewMouseHandler()

	// Track click events
	clicked := false

	// Add a clickable region
	region := &terminal.MouseRegion{
		X:      10,
		Y:      5,
		Width:  15,
		Height: 1,
		Label:  "Submit Button",
		OnClick: func(event *terminal.MouseEvent) {
			clicked = true
		},
	}
	handler.AddRegion(region)

	// Simulate a click at (12, 5) - inside the region
	pressEvent := &terminal.MouseEvent{
		X:      12,
		Y:      5,
		Button: terminal.MouseButtonLeft,
		Type:   terminal.MousePress,
	}
	handler.HandleEvent(pressEvent)

	releaseEvent := &terminal.MouseEvent{
		X:      12,
		Y:      5,
		Button: terminal.MouseButtonNone,
		Type:   terminal.MouseRelease,
	}
	handler.HandleEvent(releaseEvent)

	fmt.Printf("Button was clicked: %v\n", clicked)
}
Output:
Button was clicked: true

func NewMouseHandler

func NewMouseHandler() *MouseHandler

NewMouseHandler creates a new mouse handler

func (*MouseHandler) AddRegion

func (h *MouseHandler) AddRegion(region *MouseRegion)

AddRegion adds a clickable region

func (*MouseHandler) CancelDrag

func (h *MouseHandler) CancelDrag()

CancelDrag cancels an active drag (e.g., on Escape key)

func (*MouseHandler) ClearRegions

func (h *MouseHandler) ClearRegions()

ClearRegions removes all regions

func (*MouseHandler) DisableDebug

func (h *MouseHandler) DisableDebug()

DisableDebug disables debug logging

func (*MouseHandler) EnableDebug

func (h *MouseHandler) EnableDebug()

EnableDebug enables debug logging for mouse events

func (*MouseHandler) HandleEvent

func (h *MouseHandler) HandleEvent(event *MouseEvent)

HandleEvent processes a mouse event with full state machine

func (*MouseHandler) RemoveRegion

func (h *MouseHandler) RemoveRegion(region *MouseRegion)

RemoveRegion removes a specific region

type MouseModifiers

type MouseModifiers int

MouseModifiers represents keyboard modifiers held during mouse event

const (
	ModShift MouseModifiers = 1 << iota
	ModAlt
	ModCtrl
	ModMeta
)

type MouseRegion

type MouseRegion struct {
	X, Y          int         // Top-left position in terminal cells
	Width, Height int         // Size in terminal cells
	ZIndex        int         // Layer order (higher = on top, receives events first)
	Label         string      // Optional label for debugging
	CursorStyle   CursorStyle // Visual cursor hint

	// Event handlers - all are optional and called when corresponding events occur
	OnPress       func(event *MouseEvent) // Button pressed in region
	OnRelease     func(event *MouseEvent) // Button released in region
	OnClick       func(event *MouseEvent) // Clicked in region (press + release without much movement)
	OnDoubleClick func(event *MouseEvent) // Double-clicked in region
	OnTripleClick func(event *MouseEvent) // Triple-clicked in region
	OnEnter       func(event *MouseEvent) // Mouse entered region
	OnLeave       func(event *MouseEvent) // Mouse left region
	OnMove        func(event *MouseEvent) // Mouse moved within region (requires EnableMouseTracking)
	OnDragStart   func(event *MouseEvent) // Drag started in region
	OnDrag        func(event *MouseEvent) // Dragging within region
	OnDragEnd     func(event *MouseEvent) // Drag ended in region
	OnScroll      func(event *MouseEvent) // Mouse wheel scrolled in region
}

MouseRegion represents a rectangular clickable area on the screen. Regions can have event handlers attached and are managed by MouseHandler.

Regions support layering via ZIndex - higher ZIndex regions receive events first. This allows creating overlapping interactive areas like buttons, menus, and modals.

Example:

region := &terminal.MouseRegion{
    X: 10, Y: 5,
    Width: 20, Height: 3,
    ZIndex: 1,
    Label: "Submit Button",
    CursorStyle: terminal.CursorPointer,
    OnClick: func(event *terminal.MouseEvent) {
        fmt.Println("Button clicked!")
    },
    OnEnter: func(event *terminal.MouseEvent) {
        // Highlight button
    },
    OnLeave: func(event *terminal.MouseEvent) {
        // Remove highlight
    },
}
handler.AddRegion(region)

func (*MouseRegion) Contains

func (r *MouseRegion) Contains(x, y int) bool

Contains checks if a point is within the region's bounds. Coordinates are in terminal cells (0-based).

type PasteDisplayMode

type PasteDisplayMode int

PasteDisplayMode controls how pasted content is displayed in the terminal. This is separate from whether the paste is accepted or rejected.

const (
	// PasteDisplayNormal shows the pasted content normally (default behavior).
	// The content is echoed to the terminal as it would be if typed.
	PasteDisplayNormal PasteDisplayMode = iota

	// PasteDisplayPlaceholder shows a placeholder like "[pasted 27 lines]" instead of the content.
	// Use this to avoid cluttering the screen with large pastes.
	PasteDisplayPlaceholder

	// PasteDisplayHidden doesn't show anything (content is added silently).
	// Use this when you're handling the display yourself or for password fields.
	PasteDisplayHidden
)

type PasteHandler

type PasteHandler func(info PasteInfo) (PasteHandlerDecision, string)

PasteHandler is called when paste content is received in bracketed paste mode. It allows the application to inspect, modify, or reject pasted content before insertion.

The handler receives a PasteInfo with details about the paste and should return:

  • (PasteAccept, "") to accept the paste as-is
  • (PasteReject, "") to reject the paste completely
  • (PasteModified, newContent) to replace the paste with modified content

Example use cases:

  • Limit paste size to prevent resource exhaustion
  • Strip dangerous content or escape sequences
  • Normalize line endings or whitespace
  • Show a confirmation dialog for large pastes

Example:

handler := func(info terminal.PasteInfo) (terminal.PasteHandlerDecision, string) {
    if info.LineCount > 100 {
        // Reject pastes larger than 100 lines
        return terminal.PasteReject, ""
    }
    if strings.Contains(info.Content, "\x1b") {
        // Strip ANSI escape sequences
        cleaned := stripANSI(info.Content)
        return terminal.PasteModified, cleaned
    }
    return terminal.PasteAccept, ""
}

type PasteHandlerDecision

type PasteHandlerDecision int

PasteHandlerDecision represents the decision made by a PasteHandler about how to handle pasted content.

const (
	// PasteAccept indicates the paste should be accepted and inserted normally.
	PasteAccept PasteHandlerDecision = iota

	// PasteReject indicates the paste should be rejected completely.
	// Use this for security checks or when the paste contains invalid content.
	PasteReject

	// PasteModified indicates the paste content has been modified by the handler.
	// Return this along with the modified content string.
	PasteModified
)

type PasteInfo

type PasteInfo struct {
	Content   string // The pasted content (may contain multiple lines)
	LineCount int    // Number of lines in the paste (lines separated by \n)
	ByteCount int    // Number of bytes in the paste
}

PasteInfo contains information about a paste event that can be used by a PasteHandler to make decisions about the pasted content.

type Position

type Position struct {
	X int
	Y int
}

Position represents a cursor position

type RGB

type RGB = color.RGB

type Recorder

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

Recorder handles session recording to asciinema v2 format

func (*Recorder) RecordInput

func (r *Recorder) RecordInput(data string)

RecordInput captures user input

func (*Recorder) RecordOutput

func (r *Recorder) RecordOutput(data string)

RecordOutput captures terminal output

func (*Recorder) WriteError added in v0.0.2

func (r *Recorder) WriteError() error

WriteError returns any error that occurred during recording. Returns nil if no errors have occurred.

type RecordingEvent

type RecordingEvent struct {
	Time float64 // Seconds since recording start
	Type string  // "o" (output) or "i" (input)
	Data string  // The actual content
}

RecordingEvent represents a single event [time, type, data] Implements custom JSON marshaling for asciinema format

func (RecordingEvent) MarshalJSON

func (e RecordingEvent) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON encoding for asciinema array format

type RecordingHeader

type RecordingHeader struct {
	Version   int               `json:"version"`
	Width     int               `json:"width"`
	Height    int               `json:"height"`
	Timestamp int64             `json:"timestamp"`
	Env       map[string]string `json:"env,omitempty"`
	Title     string            `json:"title,omitempty"`
}

RecordingHeader represents asciinema v2 header (first line of .cast file)

type RecordingOptions

type RecordingOptions struct {
	Compress      bool              // Enable gzip compression
	RedactSecrets bool              // Redact passwords, tokens, etc.
	Title         string            // Recording title (metadata)
	Env           map[string]string // Environment variables (metadata)
	IdleTimeLimit float64           // Max idle time between events (0 = no limit)
}

RecordingOptions configures recording behavior

func DefaultRecordingOptions

func DefaultRecordingOptions() RecordingOptions

DefaultRecordingOptions returns sensible defaults

type RenderFrame

type RenderFrame interface {
	// SetCell sets the character and style at the given position.
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Coordinates must be within bounds: 0 <= x < width, 0 <= y < height
	//
	// Postconditions:
	//   - The cell at (x, y) will display 'char' with 'style' on EndFrame()
	//   - If the cell was previously set, old value is replaced
	//
	// Errors:
	//   - Returns ErrOutOfBounds if x or y are invalid
	//
	// Performance: O(1)
	SetCell(x, y int, char rune, style Style) error

	// PrintStyled outputs text at the specified position using the provided style.
	// Text automatically wraps to the next line when reaching the frame edge (default terminal behavior).
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Starting coordinates must be within bounds
	//
	// Postconditions:
	//   - Text will be rendered starting at (x, y)
	//   - Text automatically wraps at frame edge (character-by-character)
	//   - Newlines (\n) advance to the next line
	//
	// Performance: O(len(text))
	PrintStyled(x, y int, text string, style Style) error

	// PrintTruncated outputs text at the specified position using the provided style.
	// Text is truncated (clipped) at the frame edge without wrapping to the next line.
	//
	// Use this when you want text to be cut off at the boundary rather than wrap.
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Starting coordinates must be within bounds
	//
	// Postconditions:
	//   - Text will be rendered starting at (x, y)
	//   - Text is truncated at frame edge (no wrapping)
	//   - Newlines (\n) advance to the next line
	//
	// Performance: O(len(text))
	PrintTruncated(x, y int, text string, style Style) error

	// FillStyled fills a rectangular area with a character and a specific style.
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Rectangle should be within bounds (out-of-bounds areas are clipped)
	//
	// Postconditions:
	//   - All cells in the rectangle will display 'char' with 'style'
	//   - Cells outside terminal boundaries are ignored
	//
	// Errors:
	//   - Returns ErrOutOfBounds if any part of the rectangle is invalid
	//
	// Performance: O(width * height)
	FillStyled(x, y, width, height int, char rune, style Style) error

	// Size returns the dimensions of the frame
	//
	// Returns: (width, height) of the terminal in character cells
	//
	// Performance: O(1)
	Size() (width, height int)

	// GetBounds returns the rectangular bounds of the frame.
	// This includes the starting (Min.X, Min.Y) and ending (Max.X, Max.Y) coordinates
	// relative to the terminal's top-left corner (0,0).
	//
	// Performance: O(1)
	GetBounds() image.Rectangle

	// SubFrame returns a new RenderFrame that is a sub-rectangle of the current frame.
	// All drawing operations on the sub-frame will be clipped to its bounds
	// and translated so that (0,0) of the sub-frame corresponds to the top-left
	// of the specified rectangle within the parent frame.
	//
	// IMPORTANT: When drawing to a SubFrame, always use coordinates relative to (0,0),
	// NOT the bounds from GetBounds().Min. The SubFrame automatically handles coordinate
	// translation.
	//
	// Example:
	//   subFrame := frame.SubFrame(image.Rect(10, 5, 30, 15))
	//   subFrame.PrintStyled(0, 0, "Hello", style)  // Correct: draws at top-left of subframe
	//   // NOT: subFrame.PrintStyled(10, 5, "Hello", style)  // Wrong: would draw outside subframe
	//
	// Preconditions:
	//   - The rectangle `rect` must be relative to the parent frame's coordinates.
	//
	// Postconditions:
	//   - A new RenderFrame is returned, clipped to the intersection of `rect`
	//     and the parent frame's bounds.
	//   - The returned frame uses local coordinates starting at (0, 0).
	//
	// Performance: O(1)
	SubFrame(rect image.Rectangle) RenderFrame

	// Fill fills the entire frame with a character and style.
	// This is a convenience method equivalent to FillStyled(0, 0, width, height, char, style).
	//
	// Use this when you want to fill the entire frame without worrying about coordinates.
	Fill(char rune, style Style) error

	// PrintHyperlink outputs a clickable hyperlink using OSC 8 protocol.
	// The hyperlink will be clickable in terminals that support OSC 8 (iTerm2, WezTerm, kitty, etc.).
	// In terminals that don't support OSC 8, the escape codes are ignored and only the text is shown.
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Starting coordinates must be within bounds
	//   - Hyperlink must be valid (non-empty URL and text)
	//
	// Postconditions:
	//   - Hyperlink will be rendered starting at (x, y)
	//   - In supported terminals, clicking the link opens the URL
	//   - Text is styled according to the hyperlink's Style field
	//
	// Performance: O(len(text))
	PrintHyperlink(x, y int, link Hyperlink) error

	// PrintHyperlinkFallback outputs a hyperlink using fallback format: "Text (URL)".
	// Use this when you want to explicitly show the URL, or when you know the terminal
	// doesn't support OSC 8.
	//
	// Preconditions:
	//   - Frame must be active (between BeginFrame and EndFrame)
	//   - Starting coordinates must be within bounds
	//   - Hyperlink must be valid (non-empty URL and text)
	//
	// Postconditions:
	//   - Text and URL will be rendered starting at (x, y) in format "Text (URL)"
	//   - Text is styled according to the hyperlink's Style field
	//
	// Performance: O(len(text) + len(url))
	PrintHyperlinkFallback(x, y int, link Hyperlink) error
}

RenderFrame represents a rendering surface for a single frame. All operations on a RenderFrame are atomic within the context of that frame.

Thread Safety: RenderFrame is NOT thread-safe. Only one goroutine should use a RenderFrame at a time. The frame is obtained from BeginFrame() which locks the terminal, ensuring exclusive access.

Example

ExampleRenderFrame demonstrates using RenderFrame for drawing.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	var output strings.Builder
	term := terminal.NewTestTerminal(40, 10, &output)

	// Begin a frame
	frame, _ := term.BeginFrame()

	// Get frame dimensions
	width, height := frame.Size()
	fmt.Printf("Frame size: %dx%d\n", width, height)

	// Draw at different positions
	style := terminal.NewStyle()
	frame.PrintStyled(0, 0, "Top left", style)
	frame.PrintStyled(0, 1, "Second line", style)

	// Fill a rectangular area
	frame.FillStyled(10, 3, 5, 2, '*', style)

	// End the frame (flushes to terminal)
	term.EndFrame(frame)

}
Output:
Frame size: 40x10

type RenderMetrics

type RenderMetrics struct {

	// Frame metrics
	TotalFrames      uint64 // Total number of frames rendered
	CellsUpdated     uint64 // Total cells written to terminal
	ANSICodesEmitted uint64 // Total ANSI escape codes emitted
	BytesWritten     uint64 // Total bytes written to terminal
	SkippedFrames    uint64 // Frames skipped due to no changes

	// Timing metrics
	TotalRenderTime time.Duration // Cumulative time spent rendering
	LastFrameTime   time.Duration // Time taken for last frame
	MinFrameTime    time.Duration // Fastest frame
	MaxFrameTime    time.Duration // Slowest frame

	// Dirty region metrics
	TotalDirtyArea uint64 // Sum of all dirty region areas
	LastDirtyArea  int    // Area of last dirty region (width * height)
	MaxDirtyArea   int    // Largest dirty region seen
	// contains filtered or unexported fields
}

RenderMetrics tracks performance statistics for the terminal rendering system.

Metrics collection is disabled by default for maximum performance. Enable it using Terminal.EnableMetrics() when you need to diagnose performance issues or optimize rendering code.

Usage

term.EnableMetrics()

// Render some frames...
for i := 0; i < 100; i++ {
    frame, _ := term.BeginFrame()
    // ... draw content ...
    term.EndFrame(frame)
}

// Get performance metrics
snapshot := term.GetMetrics()
fmt.Println(snapshot.String())
// Output: Frames: 100, avg 45.2ms/frame, 1234 cells/frame, etc.

Metrics Tracked

RenderMetrics tracks:

  • Frame count (total rendered, skipped when no changes)
  • Cell updates (total cells written, average per frame)
  • ANSI codes emitted (escape sequences sent to terminal)
  • Bytes written (total output to terminal)
  • Timing (min/max/average frame render times, FPS)
  • Dirty regions (areas that changed between frames)

Thread Safety

RenderMetrics is thread-safe. Snapshot() returns a point-in-time copy that can be safely used without locking.

func NewRenderMetrics

func NewRenderMetrics() *RenderMetrics

NewRenderMetrics creates a new metrics tracker

func (*RenderMetrics) AvgCellsPerFrame

func (m *RenderMetrics) AvgCellsPerFrame() float64

AvgCellsPerFrame returns the average number of cells updated per frame

func (*RenderMetrics) AvgDirtyArea

func (m *RenderMetrics) AvgDirtyArea() float64

AvgDirtyArea returns the average dirty region area

func (*RenderMetrics) AvgFrameTime

func (m *RenderMetrics) AvgFrameTime() time.Duration

AvgFrameTime returns the average time taken per frame

func (*RenderMetrics) Efficiency

func (m *RenderMetrics) Efficiency() float64

Efficiency returns the percentage of frames that were skipped due to no changes (vs resulted in actual rendering). Higher is better when content is static.

func (*RenderMetrics) FPS

func (m *RenderMetrics) FPS() float64

FPS returns the average frames per second based on total time

func (*RenderMetrics) RecordFrame

func (m *RenderMetrics) RecordFrame(cellsUpdated int, ansiCodes int, bytesWritten int, duration time.Duration, dirtyArea int)

RecordFrame records metrics for a completed frame render

func (*RenderMetrics) RecordSkippedFrame

func (m *RenderMetrics) RecordSkippedFrame()

RecordSkippedFrame increments the skipped frame counter

func (*RenderMetrics) Reset

func (m *RenderMetrics) Reset()

Reset clears all metrics

func (*RenderMetrics) Snapshot

func (m *RenderMetrics) Snapshot() MetricsSnapshot

Snapshot returns a point-in-time copy of the metrics (thread-safe)

type Style

type Style struct {
	Foreground    Color  // Basic ANSI foreground color (or ColorDefault)
	Background    Color  // Basic ANSI background color (or ColorDefault)
	FgRGB         *RGB   // RGB override for foreground (nil = use Foreground)
	BgRGB         *RGB   // RGB override for background (nil = use Background)
	Bold          bool   // Bold or increased intensity
	Italic        bool   // Italic text
	Underline     bool   // Underlined text
	Strikethrough bool   // Strikethrough text
	Blink         bool   // Blinking text (rarely supported)
	Reverse       bool   // Reverse video (swap foreground and background)
	Hidden        bool   // Hidden text (rarely used, for passwords)
	Dim           bool   // Dim or decreased intensity
	URL           string // OSC 8 hyperlink URL (empty = no hyperlink)
}

Style represents text styling attributes including colors, text attributes, and hyperlinks.

Styles are immutable - all With* methods return a new Style with the requested changes. This makes it safe to use a base style and derive variations without affecting the original.

Colors

Colors can be specified as basic ANSI colors or full RGB:

// Basic ANSI colors (16 colors)
style := terminal.NewStyle().WithForeground(terminal.ColorRed)

// RGB colors (24-bit true color)
rgb := terminal.NewRGB(255, 100, 50)
style = style.WithFgRGB(rgb)

Text Attributes

Multiple attributes can be combined:

style := terminal.NewStyle().
    WithBold().
    WithItalic().
    WithUnderline()

Styles can include OSC 8 hyperlinks for terminals that support them:

style := terminal.NewStyle().
    WithForeground(terminal.ColorBlue).
    WithUnderline().
    WithURL("https://example.com")

Applying Styles

Styles can be applied to text or used with rendering methods:

// Apply to a string (wraps with ANSI codes)
styledText := style.Apply("Hello")
fmt.Print(styledText)

// Use with frame rendering
frame.PrintStyled(x, y, "Hello", style)

func NewStyle

func NewStyle() Style

NewStyle creates a new Style with default values (no colors, no attributes). This is the recommended starting point for building custom styles.

Example

ExampleNewStyle demonstrates creating and using text styles.

package main

import (
	"fmt"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	// Create a style with multiple attributes
	style := terminal.NewStyle().
		WithForeground(terminal.ColorRed).
		WithBackground(terminal.ColorWhite).
		WithBold().
		WithUnderline()

	// Apply the style to text
	styledText := style.Apply("Important")

	// The text is wrapped with ANSI escape codes
	fmt.Printf("Styled text has ANSI codes: %v\n", len(styledText) > len("Important"))
}
Output:
Styled text has ANSI codes: true

func (Style) Apply

func (s Style) Apply(text string) string

Apply applies the style to the given text by wrapping it with ANSI escape codes. The text is prefixed with the style's ANSI sequence and suffixed with a reset.

Example:

style := terminal.NewStyle().WithBold().WithForeground(terminal.ColorRed)
styled := style.Apply("Important")
fmt.Println(styled) // Prints "Important" in bold red

If the style is empty (no attributes set), the text is returned unchanged.

Example

ExampleStyle_Apply demonstrates applying a style to text.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	style := terminal.NewStyle().WithBold()
	text := style.Apply("Bold text")

	// The text is wrapped with ANSI codes for bold and reset
	fmt.Printf("Text contains escape codes: %v\n", strings.Contains(text, "\033["))
}
Output:
Text contains escape codes: true

func (Style) IsEmpty

func (s Style) IsEmpty() bool

IsEmpty returns true if the style has no attributes, colors, or URL set. An empty style produces no visual changes when applied.

func (Style) Merge added in v0.0.2

func (s Style) Merge(other Style) Style

Merge combines two styles, with the other style's non-default values taking precedence. This is useful for applying a base style and then overriding specific attributes.

Example:

base := terminal.NewStyle().WithBold().WithForeground(terminal.ColorBlue)
highlight := terminal.NewStyle().WithBackground(terminal.ColorYellow)
combined := base.Merge(highlight) // Bold blue text on yellow background

Attributes from 'other' override attributes from 's', but only if they are non-default. This means Merge never removes attributes, only adds or replaces them.

Example

ExampleStyle_Merge demonstrates merging two styles.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	// Base style with foreground color and bold
	base := terminal.NewStyle().
		WithForeground(terminal.ColorBlue).
		WithBold()

	// Override style with background color
	highlight := terminal.NewStyle().
		WithBackground(terminal.ColorYellow)

	// Merge: result has all attributes from both styles
	combined := base.Merge(highlight)

	// The combined style has both bold and colors
	text := combined.Apply("Highlighted")
	// Check for bold (code 1) and color codes
	hasBold := strings.Contains(text, ";1;")
	hasColors := strings.Contains(text, "34") || strings.Contains(text, "43") // Blue FG or Yellow BG
	fmt.Printf("Has styles: %v\n", hasBold || hasColors)
}
Output:
Has styles: true

func (Style) String

func (s Style) String() string

String returns the ANSI escape sequence representation of this style. The sequence includes a reset (ESC[0m) followed by all active attributes and colors. This can be printed to the terminal to activate the style.

Note: This does not include hyperlink (OSC 8) escape codes. For hyperlinks, use the Hyperlink type or Style.WithURL with frame rendering.

func (Style) WithBackground

func (s Style) WithBackground(c Color) Style

WithBackground sets the background color

func (Style) WithBgRGB

func (s Style) WithBgRGB(rgb RGB) Style

WithBgRGB sets the background RGB color

func (s Style) WithBlink() Style

WithBlink enables blinking text

func (Style) WithBold

func (s Style) WithBold() Style

WithBold enables bold text

func (Style) WithDim

func (s Style) WithDim() Style

WithDim enables dim/faint text

func (Style) WithFgRGB

func (s Style) WithFgRGB(rgb RGB) Style

WithFgRGB sets the foreground RGB color

func (Style) WithForeground

func (s Style) WithForeground(c Color) Style

WithForeground sets the foreground color

func (Style) WithItalic

func (s Style) WithItalic() Style

WithItalic enables italic text

func (Style) WithReverse

func (s Style) WithReverse() Style

WithReverse enables reverse video

func (Style) WithStrikethrough

func (s Style) WithStrikethrough() Style

WithStrikethrough enables strikethrough text

func (Style) WithURL

func (s Style) WithURL(url string) Style

WithURL sets a hyperlink URL for this style (OSC 8)

func (Style) WithUnderline

func (s Style) WithUnderline() Style

WithUnderline enables underlined text

type Terminal

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

Terminal represents a terminal interface with double-buffering capabilities.

Terminal provides a high-level interface for terminal manipulation with features like:

  • Double-buffered rendering to prevent flicker
  • Atomic frame rendering via BeginFrame/EndFrame
  • ANSI escape sequence generation
  • Raw mode and alternate screen buffer support

Thread Safety: Most methods are thread-safe. Methods for buffer operations (Size, Clear, MoveCursor, SetCell, Fill, BeginFrame/EndFrame) use locking. Methods that emit raw escape sequences during setup/teardown (HideCursor, ShowCursor, EnableAlternateScreen, EnableMouseTracking, etc.) do not lock, as they're designed to be called before/after the main rendering loop.

Lifecycle:

  1. Create with NewTerminal()
  2. Enable raw mode and alternate screen if needed
  3. Use BeginFrame/EndFrame for rendering
  4. Call Close() when done to restore terminal state

Example:

term, _ := NewTerminal()
defer term.Close()

frame, _ := term.BeginFrame()
frame.PrintStyled(0, 0, "Hello, World!", NewStyle())
term.EndFrame(frame)

func NewTerminal

func NewTerminal() (*Terminal, error)

NewTerminal creates a new Terminal instance connected to the current terminal.

The terminal is created with:

  • Double-buffering enabled for flicker-free rendering
  • Size detected from the current terminal dimensions
  • Output directed to os.Stdout
  • Default style (no colors or attributes)

Call EnableRawMode() and EnableAlternateScreen() for full-screen applications. Always call Close() when done to restore terminal state.

Example:

term, err := terminal.NewTerminal()
if err != nil {
    log.Fatal(err)
}
defer term.Close()

term.EnableRawMode()
term.EnableAlternateScreen()
// ... use terminal ...

Returns an error if the terminal size cannot be detected (e.g., when not running in a terminal). In such cases, defaults to 80x24.

Note: NewTerminal uses os.Stdout's file descriptor for terminal size detection and raw mode. This works correctly when stdout is a TTY. If stdout is piped while stdin is a TTY, raw mode may fail. For such cases, consider checking term.IsTerminal(os.Stdin.Fd()) before enabling raw mode, as done in the tui package's Runtime.Run().

func NewTestTerminal

func NewTestTerminal(width, height int, out io.Writer) *Terminal

NewTestTerminal creates a Terminal for testing with fixed dimensions and custom output.

Unlike NewTerminal(), this does not connect to a real terminal and does not query terminal size. It's useful for:

  • Unit testing rendering code
  • Capturing terminal output for inspection
  • Testing without requiring a TTY

Example:

var output strings.Builder
term := terminal.NewTestTerminal(80, 24, &output)

frame, _ := term.BeginFrame()
frame.PrintStyled(0, 0, "Test", terminal.NewStyle())
term.EndFrame(frame)

// Inspect output
result := output.String()

Note: Some terminal operations that require a file descriptor (like raw mode or resize detection) will be no-ops in test mode.

func (*Terminal) BeginFrame

func (t *Terminal) BeginFrame() (RenderFrame, error)

BeginFrame starts a new frame rendering sequence. It locks the terminal and returns a RenderFrame interface for drawing. The caller MUST call EndFrame to release the lock and flush changes.

Errors:

  • Returns ErrClosed if terminal has been closed

func (*Terminal) BypassInput

func (t *Terminal) BypassInput(text string)

BypassInput updates the terminal state to reflect input that was already echoed by the OS. This keeps the virtual buffers in sync with the physical screen.

func (*Terminal) Clear

func (t *Terminal) Clear()

Clear clears the entire screen (fills buffer with spaces)

func (*Terminal) ClearLine

func (t *Terminal) ClearLine()

ClearLine clears the current line

func (*Terminal) ClearResizeCallbacks

func (t *Terminal) ClearResizeCallbacks()

ClearResizeCallbacks removes all registered resize callbacks

func (*Terminal) ClearToEndOfLine

func (t *Terminal) ClearToEndOfLine()

ClearToEndOfLine clears from cursor to end of line

func (*Terminal) Close

func (t *Terminal) Close() error

Close cleans up terminal state and marks the terminal as closed. After Close() is called, the terminal should not be reused.

Close restores terminal modes in the correct order:

  1. Stops any active recording
  2. Disables special input modes (mouse, keyboard, paste)
  3. Shows cursor and disables alternate screen
  4. Restores normal terminal mode

func (*Terminal) CursorPosition

func (t *Terminal) CursorPosition() (x, y int)

CursorPosition returns the current virtual cursor position

func (*Terminal) DetectKittyProtocol

func (t *Terminal) DetectKittyProtocol() bool

DetectKittyProtocol probes the terminal to detect Kitty keyboard protocol support. This should be called once at startup before enabling raw mode. Returns true if the terminal supports the protocol.

The detection works by: 1. Temporarily enabling raw mode 2. Sending a query for progressive enhancement support (\x1b[?u) 3. Sending a device attributes query (\x1b[c) 4. Reading responses with a 200ms timeout

Detection is skipped when TERM indicates a multiplexer (tmux/screen) or dumb terminal, or when TERM_PROGRAM identifies a terminal known not to support the protocol (e.g. Apple Terminal). In those cases the reply would be unreliable or absent and the probe bytes could leak to the user's shell.

Caveats:

  • Detection relies on SetReadDeadline which may not work on all platforms
  • If the user types during detection, their input may be consumed
  • On terminals that don't support deadlines, detection may block briefly
  • Detection spawns a goroutine that will clean up on timeout

This is the same approach used by Gemini CLI and other terminal applications.

func (*Terminal) DisableAlternateScreen

func (t *Terminal) DisableAlternateScreen()

DisableAlternateScreen switches back to the main screen buffer

func (*Terminal) DisableBracketedPaste

func (t *Terminal) DisableBracketedPaste()

DisableBracketedPaste disables bracketed paste mode. This restores normal paste behavior where pasted text is treated as typed input.

func (*Terminal) DisableEnhancedKeyboard

func (t *Terminal) DisableEnhancedKeyboard()

DisableEnhancedKeyboard disables enhanced keyboard mode. This restores normal keyboard reporting.

func (*Terminal) DisableMetrics

func (t *Terminal) DisableMetrics()

DisableMetrics turns off performance metrics collection.

func (*Terminal) DisableMouseTracking

func (t *Terminal) DisableMouseTracking()

DisableMouseTracking disables mouse event reporting

func (*Terminal) DisableRawMode

func (t *Terminal) DisableRawMode() error

DisableRawMode disables raw terminal mode

func (*Terminal) EnableAlternateScreen

func (t *Terminal) EnableAlternateScreen()

EnableAlternateScreen switches to the alternate screen buffer

func (*Terminal) EnableBracketedPaste

func (t *Terminal) EnableBracketedPaste()

EnableBracketedPaste enables bracketed paste mode. When enabled, pasted text is wrapped in escape sequences (\033[200~ ... \033[201~) allowing the application to distinguish pasted text from typed text. This prevents security issues where pasted newlines could execute commands.

This should be called after EnableRawMode() and before reading input. Don't forget to call DisableBracketedPaste() to restore normal paste behavior.

func (*Terminal) EnableEnhancedKeyboard

func (t *Terminal) EnableEnhancedKeyboard()

EnableEnhancedKeyboard enables enhanced keyboard mode (CSI u / kitty keyboard protocol). This allows detection of modifier keys with Enter, Tab, and other special keys. For example, Shift+Enter will be reported as a distinct key event (ESC[13;2u).

If DetectKittyProtocol() was called and returned false, this does nothing. Otherwise it enables the protocol (useful when you know the terminal supports it).

Supported terminals: kitty, WezTerm, foot, ghostty, iTerm2 (3.5+), and others. Unsupported terminals will silently ignore this escape sequence.

Call DisableEnhancedKeyboard() before exiting to restore normal keyboard mode.

func (*Terminal) EnableMetrics

func (t *Terminal) EnableMetrics()

EnableMetrics turns on performance metrics collection. When enabled, the terminal will track rendering statistics including: - Cells updated per frame - ANSI escape codes emitted - Bytes written to terminal - Frame render times - Dirty region sizes

Metrics have minimal overhead but if you need maximum performance, keep them disabled (default).

Example

ExampleTerminal_EnableMetrics demonstrates performance metrics.

package main

import (
	"fmt"
	"strings"

	"github.com/deepnoodle-ai/wonton/terminal"
)

func main() {
	var output strings.Builder
	term := terminal.NewTestTerminal(40, 10, &output)

	// Enable metrics collection
	term.EnableMetrics()

	// Render some frames
	for i := 0; i < 5; i++ {
		frame, _ := term.BeginFrame()
		style := terminal.NewStyle()
		frame.PrintStyled(0, i, fmt.Sprintf("Line %d", i), style)
		term.EndFrame(frame)
	}

	// Get metrics
	snapshot := term.GetMetrics()
	fmt.Printf("Frames rendered: %d\n", snapshot.TotalFrames)
	fmt.Printf("Cells updated: %d\n", snapshot.CellsUpdated)

}
Output:
Frames rendered: 5
Cells updated: 30

func (*Terminal) EnableMouseButtons

func (t *Terminal) EnableMouseButtons()

EnableMouseButtons enables mouse button tracking without motion events. This reports button press/release events only, not mouse movement. Use this for better performance when hover detection is not needed.

func (*Terminal) EnableMouseTracking

func (t *Terminal) EnableMouseTracking()

EnableMouseTracking enables full mouse event reporting in the terminal. This includes button press/release AND all mouse motion events (hover). Use EnableMouseButtons() if you only need click events without motion tracking.

func (*Terminal) EnableRawMode

func (t *Terminal) EnableRawMode() error

EnableRawMode enables raw terminal mode for character-by-character input.

In raw mode:

  • Input is not line-buffered (characters available immediately)
  • No echo (typed characters don't appear automatically)
  • No signal generation (Ctrl+C doesn't send SIGINT)
  • Special characters (Ctrl+C, Ctrl+Z) are passed as input

This is essential for interactive applications that need immediate key responses. Always call DisableRawMode() or Close() to restore normal terminal behavior.

Example:

term, _ := terminal.NewTerminal()
defer term.Close() // Automatically disables raw mode

if err := term.EnableRawMode(); err != nil {
    log.Fatal(err)
}

// Now you can read character-by-character input
decoder := terminal.NewKeyDecoder(os.Stdin)
event, _ := decoder.ReadEvent()

Returns an error if raw mode cannot be enabled (e.g., not running in a terminal).

func (*Terminal) EndFrame

func (t *Terminal) EndFrame(f RenderFrame) error

EndFrame finishes the frame, flushes the buffer to the terminal, and unlocks.

Errors:

  • Returns ErrInvalidFrame if the frame doesn't match this terminal

func (*Terminal) Fill

func (t *Terminal) Fill(x, y, width, height int, char rune)

Fill fills a rectangular area with a character Deprecated: Use FillStyled instead.

func (*Terminal) FillStyled

func (t *Terminal) FillStyled(x, y, width, height int, char rune, style Style)

FillStyled fills a rectangular area with a character and a specific style

func (*Terminal) Flush

func (t *Terminal) Flush()

Flush calculates the difference between buffers and updates the terminal

func (*Terminal) GetCell

func (t *Terminal) GetCell(x, y int) Cell

GetCell returns the cell at the given position from the back buffer. Returns an empty cell if the position is out of bounds. This is primarily useful for testing.

func (*Terminal) GetMetrics

func (t *Terminal) GetMetrics() MetricsSnapshot

GetMetrics returns a snapshot of current rendering metrics. This is thread-safe and returns a copy of the metrics.

func (*Terminal) HideCursor

func (t *Terminal) HideCursor()

HideCursor hides the cursor

func (*Terminal) IsKittyProtocolEnabled

func (t *Terminal) IsKittyProtocolEnabled() bool

IsKittyProtocolEnabled returns true if Kitty keyboard protocol is currently enabled.

func (*Terminal) IsKittyProtocolSupported

func (t *Terminal) IsKittyProtocolSupported() bool

IsKittyProtocolSupported returns true if Kitty keyboard protocol is supported. Only valid after DetectKittyProtocol() has been called.

func (*Terminal) IsRecording

func (t *Terminal) IsRecording() bool

IsRecording returns true if a recording is active

func (*Terminal) MoveCursor

func (t *Terminal) MoveCursor(x, y int)

MoveCursor moves the cursor to the specified position

func (*Terminal) MoveCursorDown

func (t *Terminal) MoveCursorDown(n int)

MoveCursorDown moves the cursor down by n lines

func (*Terminal) MoveCursorLeft

func (t *Terminal) MoveCursorLeft(n int)

MoveCursorLeft moves the cursor left by n columns

func (*Terminal) MoveCursorRight

func (t *Terminal) MoveCursorRight(n int)

MoveCursorRight moves the cursor right by n columns

func (*Terminal) MoveCursorUp

func (t *Terminal) MoveCursorUp(n int)

MoveCursorUp moves the cursor up by n lines

func (*Terminal) OnResize

func (t *Terminal) OnResize(callback func(width, height int)) func()

OnResize registers a callback to be called when the terminal is resized. The callback receives the new width and height. Returns a function that can be called to unregister the callback.

Example:

unregister := terminal.OnResize(func(width, height int) {
    // Update layout with new dimensions
})
defer unregister()

func (*Terminal) PauseRecording

func (t *Terminal) PauseRecording()

PauseRecording temporarily suspends recording (useful for sensitive sections)

func (*Terminal) Print

func (t *Terminal) Print(text string)

Print outputs text at the current cursor position using the current style Deprecated: Use PrintStyled instead. Implicit style state will be removed in v2.0.

func (*Terminal) PrintAt

func (t *Terminal) PrintAt(x, y int, text string)

PrintAt prints text at a specific position Deprecated: Use RenderFrame.PrintStyled instead.

func (*Terminal) PrintAtStyled

func (t *Terminal) PrintAtStyled(x, y int, text string, style Style)

PrintAtStyled prints text at a specific position with a specific style

func (*Terminal) PrintStyled

func (t *Terminal) PrintStyled(text string, style Style)

PrintStyled outputs text at the current cursor position using the provided style

func (*Terminal) Println

func (t *Terminal) Println(text string)

Println outputs text with a newline Deprecated: Use PrintStyled instead.

func (*Terminal) RefreshSize

func (t *Terminal) RefreshSize() error

RefreshSize updates the cached terminal size and resizes buffers. It automatically calls all registered resize callbacks when the terminal size changes.

func (*Terminal) Reset

func (t *Terminal) Reset()

Reset resets all terminal attributes Deprecated: Use explicit styles.

func (*Terminal) ResetMetrics

func (t *Terminal) ResetMetrics()

ResetMetrics clears all accumulated metrics.

func (*Terminal) RestoreCursor

func (t *Terminal) RestoreCursor()

RestoreCursor restores the saved cursor position

func (*Terminal) ResumeRecording

func (t *Terminal) ResumeRecording()

ResumeRecording resumes a paused recording

func (*Terminal) SaveCursor

func (t *Terminal) SaveCursor()

SaveCursor saves the current cursor position

func (*Terminal) SetCell

func (t *Terminal) SetCell(x, y int, char rune, style Style) error

SetCell directly sets a cell in the buffer

func (*Terminal) SetStyle

func (t *Terminal) SetStyle(style Style)

SetStyle sets the current style for subsequent Print calls Deprecated: Use PrintStyled instead. Implicit style state will be removed in v2.0.

func (*Terminal) ShowCursor

func (t *Terminal) ShowCursor()

ShowCursor shows the cursor

func (*Terminal) Size

func (t *Terminal) Size() (width, height int)

Size returns the terminal dimensions

func (*Terminal) StartRecording

func (t *Terminal) StartRecording(filename string, opts RecordingOptions) error

StartRecording begins recording a session to the specified file

func (*Terminal) StopRecording

func (t *Terminal) StopRecording() error

StopRecording finalizes and closes the recording. Returns any error encountered during recording or while closing.

func (*Terminal) StopWatchResize

func (t *Terminal) StopWatchResize()

StopWatchResize stops watching for resize signals

func (*Terminal) WatchResize

func (t *Terminal) WatchResize()

WatchResize starts watching for terminal resize signals (SIGWINCH) and automatically updates the terminal dimensions when the window is resized. Call StopWatchResize() or Close() to stop watching.

func (*Terminal) WriteRaw

func (t *Terminal) WriteRaw(data []byte) error

WriteRaw writes raw bytes directly to the terminal output. This is thread-safe and bypasses the double-buffering system. Use this for playback or other cases where you need direct terminal output.

Jump to

Keyboard shortcuts

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