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 ¶
- Constants
- Variables
- func End() string
- func Fallback(targetURL, text string) string
- func Format(targetURL, text string) string
- func FormatWithID(targetURL, text, id string) string
- func OSC8End() string
- func OSC8Start(url string) string
- func RedactCredentials(input string) string
- func Start(targetURL string) string
- func StartWithID(targetURL, id string) string
- func StripOSC8(text string) string
- func ValidateAbsoluteURL(targetURL string) error
- func ValidateURL(targetURL string) error
- type Alignment
- type BorderStyle
- type Box
- type Cell
- type Color
- type CursorStyle
- type DirtyRegion
- type Event
- type Frame
- type Hyperlink
- type Key
- type KeyDecoder
- type KeyEvent
- type MetricsSnapshot
- type MouseButton
- type MouseEvent
- type MouseEventType
- type MouseHandler
- func (h *MouseHandler) AddRegion(region *MouseRegion)
- func (h *MouseHandler) CancelDrag()
- func (h *MouseHandler) ClearRegions()
- func (h *MouseHandler) DisableDebug()
- func (h *MouseHandler) EnableDebug()
- func (h *MouseHandler) HandleEvent(event *MouseEvent)
- func (h *MouseHandler) RemoveRegion(region *MouseRegion)
- type MouseModifiers
- type MouseRegion
- type PasteDisplayMode
- type PasteHandler
- type PasteHandlerDecision
- type PasteInfo
- type Position
- type RGB
- type Recorder
- type RecordingEvent
- type RecordingHeader
- type RecordingOptions
- type RenderFrame
- type RenderMetrics
- func (m *RenderMetrics) AvgCellsPerFrame() float64
- func (m *RenderMetrics) AvgDirtyArea() float64
- func (m *RenderMetrics) AvgFrameTime() time.Duration
- func (m *RenderMetrics) Efficiency() float64
- func (m *RenderMetrics) FPS() float64
- func (m *RenderMetrics) RecordFrame(cellsUpdated int, ansiCodes int, bytesWritten int, duration time.Duration, ...)
- func (m *RenderMetrics) RecordSkippedFrame()
- func (m *RenderMetrics) Reset()
- func (m *RenderMetrics) Snapshot() MetricsSnapshot
- type Style
- func (s Style) Apply(text string) string
- func (s Style) IsEmpty() bool
- func (s Style) Merge(other Style) Style
- func (s Style) String() string
- func (s Style) WithBackground(c Color) Style
- func (s Style) WithBgRGB(rgb RGB) Style
- func (s Style) WithBlink() Style
- func (s Style) WithBold() Style
- func (s Style) WithDim() Style
- func (s Style) WithFgRGB(rgb RGB) Style
- func (s Style) WithForeground(c Color) Style
- func (s Style) WithItalic() Style
- func (s Style) WithReverse() Style
- func (s Style) WithStrikethrough() Style
- func (s Style) WithURL(url string) Style
- func (s Style) WithUnderline() Style
- type Terminal
- func (t *Terminal) BeginFrame() (RenderFrame, error)
- func (t *Terminal) BypassInput(text string)
- func (t *Terminal) Clear()
- func (t *Terminal) ClearLine()
- func (t *Terminal) ClearResizeCallbacks()
- func (t *Terminal) ClearToEndOfLine()
- func (t *Terminal) Close() error
- func (t *Terminal) CursorPosition() (x, y int)
- func (t *Terminal) DetectKittyProtocol() bool
- func (t *Terminal) DisableAlternateScreen()
- func (t *Terminal) DisableBracketedPaste()
- func (t *Terminal) DisableEnhancedKeyboard()
- func (t *Terminal) DisableMetrics()
- func (t *Terminal) DisableMouseTracking()
- func (t *Terminal) DisableRawMode() error
- func (t *Terminal) EnableAlternateScreen()
- func (t *Terminal) EnableBracketedPaste()
- func (t *Terminal) EnableEnhancedKeyboard()
- func (t *Terminal) EnableMetrics()
- func (t *Terminal) EnableMouseButtons()
- func (t *Terminal) EnableMouseTracking()
- func (t *Terminal) EnableRawMode() error
- func (t *Terminal) EndFrame(f RenderFrame) error
- func (t *Terminal) Fill(x, y, width, height int, char rune)
- func (t *Terminal) FillStyled(x, y, width, height int, char rune, style Style)
- func (t *Terminal) Flush()
- func (t *Terminal) GetCell(x, y int) Cell
- func (t *Terminal) GetMetrics() MetricsSnapshot
- func (t *Terminal) HideCursor()
- func (t *Terminal) IsKittyProtocolEnabled() bool
- func (t *Terminal) IsKittyProtocolSupported() bool
- func (t *Terminal) IsRecording() bool
- func (t *Terminal) MoveCursor(x, y int)
- func (t *Terminal) MoveCursorDown(n int)
- func (t *Terminal) MoveCursorLeft(n int)
- func (t *Terminal) MoveCursorRight(n int)
- func (t *Terminal) MoveCursorUp(n int)
- func (t *Terminal) OnResize(callback func(width, height int)) func()
- func (t *Terminal) PauseRecording()
- func (t *Terminal) Print(text string)
- func (t *Terminal) PrintAt(x, y int, text string)
- func (t *Terminal) PrintAtStyled(x, y int, text string, style Style)
- func (t *Terminal) PrintStyled(text string, style Style)
- func (t *Terminal) Println(text string)
- func (t *Terminal) RefreshSize() error
- func (t *Terminal) Reset()
- func (t *Terminal) ResetMetrics()
- func (t *Terminal) RestoreCursor()
- func (t *Terminal) ResumeRecording()
- func (t *Terminal) SaveCursor()
- func (t *Terminal) SetCell(x, y int, char rune, style Style) error
- func (t *Terminal) SetStyle(style Style)
- func (t *Terminal) ShowCursor()
- func (t *Terminal) Size() (width, height int)
- func (t *Terminal) StartRecording(filename string, opts RecordingOptions) error
- func (t *Terminal) StopRecording() error
- func (t *Terminal) StopWatchResize()
- func (t *Terminal) WatchResize()
- func (t *Terminal) WriteRaw(data []byte) error
Examples ¶
Constants ¶
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 ¶
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
var ( NewRGB = color.NewRGB Gradient = color.Gradient RainbowGradient = color.RainbowGradient SmoothRainbow = color.SmoothRainbow MultiGradient = color.MultiGradient )
Re-export color functions for backward compatibility
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 ¶
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 ¶
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 ¶
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 ¶
OSC8Start returns the OSC 8 escape sequence to start a hyperlink. Format: \033]8;;URL\033\\
func RedactCredentials ¶
RedactCredentials can be called manually to test redaction
func Start ¶
Start returns the OSC 8 escape sequence to start a hyperlink. Format: ESC ] 8 ; ; URL ST Where ST (String Terminator) is ESC \
func StartWithID ¶
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 ¶
StripOSC8 removes OSC 8 hyperlink escape sequences from text. This is useful for getting plain text from hyperlink-formatted strings.
func ValidateAbsoluteURL ¶
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 ¶
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 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 (*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 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) 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 ¶
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 (*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) WithTitleStyle ¶
WithTitleStyle sets the title style
type Hyperlink ¶
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.
Terminal Support ¶
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.
Usage ¶
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)
Styling ¶
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 ¶
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)
Example ¶
ExampleNewHyperlink demonstrates creating clickable hyperlinks.
package main
import (
"fmt"
"strings"
"github.com/deepnoodle-ai/wonton/terminal"
)
func main() {
// Create a hyperlink with default styling (blue, underlined)
link := terminal.NewHyperlink("https://golang.org", "Go Website")
// Hyperlinks can be printed using RenderFrame.PrintHyperlink
// or formatted with Format() for direct printing
formatted := link.Format()
// The formatted string contains OSC 8 escape codes
fmt.Printf("Contains OSC 8: %v\n", strings.Contains(formatted, "\033]8;"))
}
Output: Contains OSC 8: true
func (Hyperlink) Format ¶
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 ¶
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 ¶
FormatWithOption returns either the OSC 8 formatted hyperlink or the fallback, depending on the useOSC8 parameter.
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:
- A special key press (arrows, function keys, etc.) - check the Key field
- A printable character - check the Rune field
- 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)
}
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:
- The handler finds the topmost region (highest ZIndex) under the cursor
- If the region changed, OnLeave is called on the old region and OnEnter on the new
- The event is dispatched to the appropriate handler (OnClick, OnDrag, etc.)
- Click detection tracks press/release to synthesize click events
- 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 Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder handles session recording to asciinema v2 format
func (*Recorder) RecordInput ¶
RecordInput captures user input
func (*Recorder) RecordOutput ¶
RecordOutput captures terminal output
func (*Recorder) WriteError ¶ added in v0.0.2
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) 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()
Hyperlinks ¶
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 ¶
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 ¶
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
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 ¶
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 ¶
WithBackground sets the background color
func (Style) WithForeground ¶
WithForeground sets the foreground color
func (Style) WithStrikethrough ¶
WithStrikethrough enables strikethrough text
func (Style) WithUnderline ¶
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:
- Create with NewTerminal()
- Enable raw mode and alternate screen if needed
- Use BeginFrame/EndFrame for rendering
- 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 ¶
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 ¶
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 ¶
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) 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 ¶
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:
- Stops any active recording
- Disables special input modes (mouse, keyboard, paste)
- Shows cursor and disables alternate screen
- Restores normal terminal mode
func (*Terminal) CursorPosition ¶
CursorPosition returns the current virtual cursor position
func (*Terminal) DetectKittyProtocol ¶
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 ¶
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 ¶
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 ¶
Fill fills a rectangular area with a character Deprecated: Use FillStyled instead.
func (*Terminal) FillStyled ¶
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 ¶
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) IsKittyProtocolEnabled ¶
IsKittyProtocolEnabled returns true if Kitty keyboard protocol is currently enabled.
func (*Terminal) IsKittyProtocolSupported ¶
IsKittyProtocolSupported returns true if Kitty keyboard protocol is supported. Only valid after DetectKittyProtocol() has been called.
func (*Terminal) IsRecording ¶
IsRecording returns true if a recording is active
func (*Terminal) MoveCursor ¶
MoveCursor moves the cursor to the specified position
func (*Terminal) MoveCursorDown ¶
MoveCursorDown moves the cursor down by n lines
func (*Terminal) MoveCursorLeft ¶
MoveCursorLeft moves the cursor left by n columns
func (*Terminal) MoveCursorRight ¶
MoveCursorRight moves the cursor right by n columns
func (*Terminal) MoveCursorUp ¶
MoveCursorUp moves the cursor up by n lines
func (*Terminal) OnResize ¶
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 ¶
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 ¶
PrintAt prints text at a specific position Deprecated: Use RenderFrame.PrintStyled instead.
func (*Terminal) PrintAtStyled ¶
PrintAtStyled prints text at a specific position with a specific style
func (*Terminal) PrintStyled ¶
PrintStyled outputs text at the current cursor position using the provided style
func (*Terminal) RefreshSize ¶
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) SetStyle ¶
SetStyle sets the current style for subsequent Print calls Deprecated: Use PrintStyled instead. Implicit style state will be removed in v2.0.
func (*Terminal) StartRecording ¶
func (t *Terminal) StartRecording(filename string, opts RecordingOptions) error
StartRecording begins recording a session to the specified file
func (*Terminal) StopRecording ¶
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.