tty

package
v0.74.1 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package tty provides an embeddable interactive tmux component for sending keystrokes, mouse events, and clipboard paste to a tmux pane while capturing and rendering its output.

Package tty provides an embeddable tmux terminal model for TUI plugins. It handles tmux session management, key mapping, cursor rendering, and adaptive polling.

Index

Constants

View Source
const (
	// PollingDecayFast is the polling interval during active typing.
	PollingDecayFast = 50 * time.Millisecond

	// PollingDecayMedium is the polling interval after brief inactivity.
	PollingDecayMedium = 200 * time.Millisecond

	// PollingDecaySlow is the polling interval after extended inactivity.
	PollingDecaySlow = 250 * time.Millisecond

	// KeystrokeDebounce delays polling after keystrokes to batch rapid typing.
	// Allows typing bursts to coalesce into fewer polls, reducing CPU usage.
	KeystrokeDebounce = 20 * time.Millisecond

	// InactivityMediumThreshold triggers medium polling.
	InactivityMediumThreshold = 2 * time.Second

	// InactivitySlowThreshold triggers slow polling.
	InactivitySlowThreshold = 10 * time.Second
)

Polling interval constants for adaptive polling

View Source
const (
	BracketedPasteEnable  = "\x1b[?2004h" // ESC[?2004h - app enables bracketed paste
	BracketedPasteDisable = "\x1b[?2004l" // ESC[?2004l - app disables bracketed paste
	BracketedPasteStart   = "\x1b[200~"   // ESC[200~ - start of pasted content
	BracketedPasteEnd     = "\x1b[201~"   // ESC[201~ - end of pasted content

	MouseModeEnable1000  = "\x1b[?1000h"
	MouseModeEnable1002  = "\x1b[?1002h"
	MouseModeEnable1003  = "\x1b[?1003h"
	MouseModeEnable1006  = "\x1b[?1006h"
	MouseModeEnable1015  = "\x1b[?1015h"
	MouseModeDisable1000 = "\x1b[?1000l"
	MouseModeDisable1002 = "\x1b[?1002l"
	MouseModeDisable1003 = "\x1b[?1003l"
	MouseModeDisable1006 = "\x1b[?1006l"
	MouseModeDisable1015 = "\x1b[?1015l"
)

Terminal mode escape sequences

View Source
const (
	// DoubleEscapeDelay is the max time between Escape presses for double-escape exit.
	// Single Escape is delayed by this amount to detect double-press.
	DoubleEscapeDelay = 150 * time.Millisecond
)

Interactive mode timing constants

Variables

View Source
var (

	// partialMouseSeqRegex matches SGR mouse sequences that lost their ESC prefix
	// due to split-read timing in terminal input.
	PartialMouseSeqRegex = regexp.MustCompile(`^(\[<\d+;\d+;\d+[Mm])+$`)
)

Regexes for cleaning terminal output

Functions

func CalculatePollingInterval

func CalculatePollingInterval(lastActivityTime time.Time) time.Duration

CalculatePollingInterval determines the appropriate polling interval based on the time since the last user activity.

func CapturePaneOutput

func CapturePaneOutput(target string, scrollback int) (string, error)

CapturePaneOutput captures the current output of a tmux pane. Uses capture-pane with -p flag to print to stdout and -e to preserve ANSI escape sequences (colors, styles). The scrollback parameter controls how many lines of history to capture.

func ContainsMouseSequence

func ContainsMouseSequence(s string) bool

ContainsMouseSequence checks if input looks like it contains SGR mouse data (td-e2ce50). More lenient than PartialMouseSeqRegex - catches truncated/split sequences. Used to filter spurious key events during fast scrolling.

func CursorStyle

func CursorStyle() lipgloss.Style

CursorStyle returns the cursor style using current theme colors. Uses bold reverse video with a bright background for maximum visibility. The bright cyan/white combination stands out against most terminal backgrounds including Claude Code's diff highlighting and colored output.

func DetectBracketedPasteMode

func DetectBracketedPasteMode(output string) bool

DetectBracketedPasteMode checks captured output to determine if the app has enabled bracketed paste mode. Looks for the most recent occurrence of either the enable (ESC[?2004h) or disable (ESC[?2004l) sequence.

func DetectMouseReportingMode

func DetectMouseReportingMode(output string) bool

DetectMouseReportingMode checks captured output to determine if the app has enabled mouse reporting. Looks for the most recent occurrence of enable vs disable sequences across all mouse mode types.

func ExtractUnknownCSIBytes

func ExtractUnknownCSIBytes(msg interface{}) []byte

ExtractUnknownCSIBytes checks whether msg is a BubbleTea unknownCSISequenceMsg (an unexported []byte type) containing a CSI sequence, and returns the raw bytes if so. Returns nil for any other message type.

func IsPasteInput

func IsPasteInput(msg tea.KeyMsg) bool

IsPasteInput detects if the input is a paste operation. Returns true if the input contains newlines or is longer than a typical typed sequence.

func IsSessionDeadError

func IsSessionDeadError(err error) bool

IsSessionDeadError checks if an error indicates the tmux session/pane is gone.

func LooksLikeMouseFragment

func LooksLikeMouseFragment(s string) bool

LooksLikeMouseFragment checks if input could be a fragment of an SGR mouse sequence (td-e2ce50). This is even more lenient than ContainsMouseSequence - catches very short fragments like "[<" or "M[<" that occur when terminal splits mouse events across reads. Used to suppress snap-back and key forwarding during fast scrolling.

func MapKeyToTmux

func MapKeyToTmux(msg tea.KeyMsg) (key string, useLiteral bool)

MapKeyToTmux translates a Bubble Tea key message to a tmux send-keys argument. Returns the tmux key name and whether to use literal mode (-l). For modified keys and special keys, returns the tmux key name. For literal characters, returns the character with useLiteral=true.

func NormalizeToCSIu

func NormalizeToCSIu(raw []byte) string

NormalizeToCSIu converts an unknown CSI sequence to CSI u format for forwarding to tmux. It handles:

CSI u:             ESC [ keycode ; modifier u  → passed through
modifyOtherKeys:   ESC [ 27 ; modifier ; keycode ~  → converted to CSI u

Returns the CSI u formatted string, or empty string if unrecognized.

func PasteClipboardToTmuxCmd

func PasteClipboardToTmuxCmd(sessionName string, bracketed bool) tea.Cmd

PasteClipboardToTmuxCmd returns a tea.Cmd that pastes clipboard content to a tmux session. The bracketed parameter determines whether to use bracketed paste mode. Returns a PasteResultMsg with the result.

func QueryCursorPositionSync

func QueryCursorPositionSync(target string) (row, col, paneHeight, paneWidth int, visible, ok bool)

QueryCursorPositionSync synchronously queries cursor position for the given target. Used to capture cursor position atomically with output in poll goroutines. Returns row, col (0-indexed), paneHeight, paneWidth, visible, and ok (false if query failed). paneHeight is needed to calculate cursor offset when display height differs from pane height.

func QueryPaneSize

func QueryPaneSize(target string) (width, height int, ok bool)

QueryPaneSize queries the current size of a tmux pane.

func RenderWithCursor

func RenderWithCursor(content string, cursorRow, cursorCol int, visible bool) string

RenderWithCursor overlays the cursor on content at the specified position. cursorRow is relative to the visible content (0 = first visible line). cursorCol is the column within the line (0-indexed). Preserves ANSI escape codes in surrounding content while rendering cursor.

func ResizeTmuxPane

func ResizeTmuxPane(paneID string, width, height int)

ResizeTmuxPane resizes a tmux window/pane to the specified dimensions. resize-window works for detached sessions; resize-pane is a fallback.

func SendBracketedPasteToTmux

func SendBracketedPasteToTmux(sessionName, text string) error

SendBracketedPasteToTmux sends text wrapped in bracketed paste sequences. Used when the target app has enabled bracketed paste mode.

func SendKeyToTmux

func SendKeyToTmux(sessionName, key string) error

SendKeyToTmux sends a key to a tmux pane using send-keys. Uses the tmux key name syntax (e.g., "Enter", "C-c", "Up").

func SendKeysCmd

func SendKeysCmd(sessionName string, keys ...KeySpec) tea.Cmd

SendKeysCmd sends keys to tmux asynchronously. Keys are sent in order within a single goroutine to prevent reordering. Returns SessionDeadMsg if the session has ended.

func SendLiteralToTmux

func SendLiteralToTmux(sessionName, text string) error

SendLiteralToTmux sends literal text to a tmux pane using send-keys -l. This prevents tmux from interpreting special key names.

func SendPasteInputCmd

func SendPasteInputCmd(sessionName, text string, bracketed bool) tea.Cmd

SendPasteInputCmd sends paste text to tmux asynchronously. Used for multi-character terminal input (not clipboard paste which is already async).

func SendPasteToTmux

func SendPasteToTmux(sessionName, text string) error

SendPasteToTmux pastes multi-line text via tmux buffer. Uses load-buffer + paste-buffer which works regardless of app paste mode state.

func SendSGRMouse

func SendSGRMouse(sessionName string, button, col, row int, release bool) error

SendSGRMouse sends an SGR mouse event to a tmux pane. button is the mouse button (0=left, 1=middle, 2=right). col and row are 1-indexed coordinates. release indicates if this is a button release event.

func SetWindowSizeManual

func SetWindowSizeManual(sessionName string)

SetWindowSizeManual sets the tmux window-size option to "manual" for a session. This prevents tmux from auto-constraining window size based on attached clients, allowing resize-window commands to stick reliably.

Types

type CaptureResultMsg

type CaptureResultMsg struct {
	Target string // Pane or session this capture is for
	Output string // Captured output (empty on error)
	Err    error  // Non-nil if capture failed

	// Cursor state captured atomically with output
	CursorRow     int
	CursorCol     int
	CursorVisible bool
	PaneHeight    int
	PaneWidth     int
}

CaptureResultMsg delivers async tmux capture results. Used to avoid blocking the UI thread on tmux subprocess calls.

type Config

type Config struct {
	// ExitKey is the keybinding to exit interactive mode (default: "ctrl+\\").
	ExitKey string

	// AttachKey is the keybinding to attach to the full tmux session (default: "ctrl+]").
	AttachKey string

	// CopyKey is the keybinding to copy selection (default: "alt+c").
	CopyKey string

	// PasteKey is the keybinding to paste clipboard (default: "alt+v").
	PasteKey string

	// ScrollbackLines is the number of scrollback lines to capture (default: 600).
	ScrollbackLines int
}

Config holds configuration options for a tty Model.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the default configuration.

type CursorPositionMsg

type CursorPositionMsg struct {
	Row     int
	Col     int
	Visible bool
}

CursorPositionMsg delivers cursor position from async query.

type EscapeTimerMsg

type EscapeTimerMsg struct{}

EscapeTimerMsg is sent when the escape delay timer fires. If pendingEscape is still true, we forward the single Escape to tmux.

type KeySpec

type KeySpec struct {
	Value   string
	Literal bool
}

KeySpec describes a key to send to tmux with ordering preserved.

type Model

type Model struct {
	Config Config
	State  *State

	// Width and Height are set by the containing plugin
	Width  int
	Height int

	// Callbacks for plugin integration
	OnExit   func() tea.Cmd // Called when user exits interactive mode
	OnAttach func() tea.Cmd // Called when user requests full tmux attach
}

Model is an embeddable component that provides interactive tmux functionality. Plugins embed this Model and delegate Update/View when interactive mode is active.

func New

func New(config *Config) *Model

New creates a new tty Model with the given configuration. If config is nil, DefaultConfig() is used.

func (*Model) Enter

func (m *Model) Enter(sessionName, paneID string) tea.Cmd

Enter enters interactive mode for the specified tmux session/pane. Returns a tea.Cmd to start polling for output.

func (*Model) Exit

func (m *Model) Exit()

Exit exits interactive mode.

func (*Model) GetTarget

func (m *Model) GetTarget() string

GetTarget returns the current tmux target (pane ID or session name).

func (*Model) IsActive

func (m *Model) IsActive() bool

IsActive returns whether interactive mode is currently active.

func (*Model) ResizeAndPollImmediate

func (m *Model) ResizeAndPollImmediate(width, height int) tea.Cmd

ResizeAndPollImmediate updates dimensions and triggers an immediate resize and poll. Unlike SetDimensions, this bypasses debouncing for use with WindowSizeMsg. The resize and poll are batched so the view updates immediately after resize.

func (*Model) SetDimensions

func (m *Model) SetDimensions(width, height int) tea.Cmd

SetDimensions updates the view dimensions for resize handling.

func (*Model) Update

func (m *Model) Update(msg tea.Msg) tea.Cmd

Update handles messages in interactive mode. Returns the updated model and any commands to execute. Plugins should call this when they receive messages and interactive mode is active.

func (*Model) View

func (m *Model) View() string

View renders the interactive terminal content with cursor overlay. Plugins should call this to render the terminal when interactive mode is active.

type OutputBuffer

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

OutputBuffer is a thread-safe bounded buffer for terminal output. Uses maphash for efficient content change detection to avoid duplicate processing.

func NewOutputBuffer

func NewOutputBuffer(capacity int) *OutputBuffer

NewOutputBuffer creates a new output buffer with the given capacity.

func (*OutputBuffer) Clear

func (b *OutputBuffer) Clear()

Clear removes all lines from the buffer.

func (*OutputBuffer) Len

func (b *OutputBuffer) Len() int

Len returns the number of lines in the buffer.

func (*OutputBuffer) LineCount

func (b *OutputBuffer) LineCount() int

LineCount returns the number of lines without copying.

func (*OutputBuffer) Lines

func (b *OutputBuffer) Lines() []string

Lines returns a copy of all lines in the buffer.

func (*OutputBuffer) LinesRange

func (b *OutputBuffer) LinesRange(start, end int) []string

LinesRange returns a copy of lines in the specified range [start, end). This is more efficient than Lines() when only a portion is needed.

func (*OutputBuffer) String

func (b *OutputBuffer) String() string

String returns the buffer contents as a single string.

func (*OutputBuffer) Update

func (b *OutputBuffer) Update(content string) bool

Update replaces buffer content if it has changed (detected via hash). Returns true if content was updated, false if content was unchanged.

func (*OutputBuffer) Write

func (b *OutputBuffer) Write(content string)

Write replaces content in the buffer (for backward compatibility). Prefer Update() for change detection.

type PaneResizedMsg

type PaneResizedMsg struct{}

PaneResizedMsg is sent when a pane resize operation completes. Triggers a fresh poll so captured content reflects the new width/wrapping.

type PasteResultMsg

type PasteResultMsg struct {
	Err         error // Non-nil if paste failed
	Empty       bool  // True if clipboard was empty
	SessionDead bool  // True if session died during paste
}

PasteResultMsg is sent after a paste operation completes.

type PollTickMsg

type PollTickMsg struct {
	Target     string // Which pane/session to poll
	Generation int    // For invalidating stale polls
}

PollTickMsg is sent to trigger a poll for output updates.

type SessionDeadMsg

type SessionDeadMsg struct{}

SessionDeadMsg indicates the tmux session has ended. Sent when send-keys or capture fails with a session/pane not found error.

type State

type State struct {
	// Active indicates whether interactive mode is currently active.
	Active bool

	// TargetPane is the tmux pane ID (e.g., "%12") receiving input.
	TargetPane string

	// TargetSession is the tmux session name for the active pane.
	TargetSession string

	// LastKeyTime tracks when the last key was sent for polling decay.
	LastKeyTime time.Time

	// Escape handling state
	EscapePressed      bool
	EscapeTime         time.Time
	EscapeTimerPending bool

	// LastMouseEventTime tracks when the last tea.MouseMsg was received,
	// used to suppress split-CSI "[" that leaks from mouse sequences.
	LastMouseEventTime time.Time

	// Cursor state (updated asynchronously via CaptureResultMsg)
	CursorRow     int
	CursorCol     int
	CursorVisible bool
	PaneHeight    int
	PaneWidth     int

	// Terminal mode state (updated from captured output)
	BracketedPasteEnabled bool
	MouseReportingEnabled bool

	// Visible buffer range for selection mapping
	VisibleStart     int
	VisibleEnd       int
	ContentRowOffset int

	// Resize debouncing
	LastResizeAt time.Time

	// Output buffer
	OutputBuf *OutputBuffer

	// Poll generation for invalidating stale polls
	PollGeneration int
}

State tracks the interactive mode state for a tmux session.

Jump to

Keyboard shortcuts

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