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
- Variables
- func CalculatePollingInterval(lastActivityTime time.Time) time.Duration
- func CapturePaneOutput(target string, scrollback int) (string, error)
- func ContainsMouseSequence(s string) bool
- func CursorStyle() lipgloss.Style
- func DetectBracketedPasteMode(output string) bool
- func DetectMouseReportingMode(output string) bool
- func ExtractUnknownCSIBytes(msg interface{}) []byte
- func IsPasteInput(msg tea.KeyMsg) bool
- func IsSessionDeadError(err error) bool
- func LooksLikeMouseFragment(s string) bool
- func MapKeyToTmux(msg tea.KeyMsg) (key string, useLiteral bool)
- func NormalizeToCSIu(raw []byte) string
- func PasteClipboardToTmuxCmd(sessionName string, bracketed bool) tea.Cmd
- func QueryCursorPositionSync(target string) (row, col, paneHeight, paneWidth int, visible, ok bool)
- func QueryPaneSize(target string) (width, height int, ok bool)
- func RenderWithCursor(content string, cursorRow, cursorCol int, visible bool) string
- func ResizeTmuxPane(paneID string, width, height int)
- func SendBracketedPasteToTmux(sessionName, text string) error
- func SendKeyToTmux(sessionName, key string) error
- func SendKeysCmd(sessionName string, keys ...KeySpec) tea.Cmd
- func SendLiteralToTmux(sessionName, text string) error
- func SendPasteInputCmd(sessionName, text string, bracketed bool) tea.Cmd
- func SendPasteToTmux(sessionName, text string) error
- func SendSGRMouse(sessionName string, button, col, row int, release bool) error
- func SetWindowSizeManual(sessionName string)
- type CaptureResultMsg
- type Config
- type CursorPositionMsg
- type EscapeTimerMsg
- type KeySpec
- type Model
- func (m *Model) Enter(sessionName, paneID string) tea.Cmd
- func (m *Model) Exit()
- func (m *Model) GetTarget() string
- func (m *Model) IsActive() bool
- func (m *Model) ResizeAndPollImmediate(width, height int) tea.Cmd
- func (m *Model) SetDimensions(width, height int) tea.Cmd
- func (m *Model) Update(msg tea.Msg) tea.Cmd
- func (m *Model) View() string
- type OutputBuffer
- func (b *OutputBuffer) Clear()
- func (b *OutputBuffer) Len() int
- func (b *OutputBuffer) LineCount() int
- func (b *OutputBuffer) Lines() []string
- func (b *OutputBuffer) LinesRange(start, end int) []string
- func (b *OutputBuffer) String() string
- func (b *OutputBuffer) Update(content string) bool
- func (b *OutputBuffer) Write(content string)
- type PaneResizedMsg
- type PasteResultMsg
- type PollTickMsg
- type SessionDeadMsg
- type State
Constants ¶
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
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
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 ¶
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 ¶
CalculatePollingInterval determines the appropriate polling interval based on the time since the last user activity.
func CapturePaneOutput ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
IsSessionDeadError checks if an error indicates the tmux session/pane is gone.
func LooksLikeMouseFragment ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
QueryPaneSize queries the current size of a tmux pane.
func RenderWithCursor ¶
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 ¶
ResizeTmuxPane resizes a tmux window/pane to the specified dimensions. resize-window works for detached sessions; resize-pane is a fallback.
func SendBracketedPasteToTmux ¶
SendBracketedPasteToTmux sends text wrapped in bracketed paste sequences. Used when the target app has enabled bracketed paste mode.
func SendKeyToTmux ¶
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 ¶
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 ¶
SendLiteralToTmux sends literal text to a tmux pane using send-keys -l. This prevents tmux from interpreting special key names.
func SendPasteInputCmd ¶
SendPasteInputCmd sends paste text to tmux asynchronously. Used for multi-character terminal input (not clipboard paste which is already async).
func SendPasteToTmux ¶
SendPasteToTmux pastes multi-line text via tmux buffer. Uses load-buffer + paste-buffer which works regardless of app paste mode state.
func SendSGRMouse ¶
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.
type CursorPositionMsg ¶
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 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 ¶
New creates a new tty Model with the given configuration. If config is nil, DefaultConfig() is used.
func (*Model) Enter ¶
Enter enters interactive mode for the specified tmux session/pane. Returns a tea.Cmd to start polling for output.
func (*Model) ResizeAndPollImmediate ¶
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 ¶
SetDimensions updates the view dimensions for resize handling.
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.