import type { Terminal } from "./terminal"; import { visibleWidth } from "./utils"; export type CellRect = Readonly<{ column: number; row: number; width: number; height: number; }>; export type RasterLeaseToken = Readonly<{ ownerId: string; generation: number; rect: CellRect; }>; export type RasterLeaseInvalidatedNotification = Readonly<{ type: "raster-lease-invalidated"; queueId: number; token: RasterLeaseToken; cause: "intersecting-generic-output" | "full-redraw" | "resize" | "terminal-loss" | "capability-loss" | "mode-off" | "dispose" | "explicit" | "manual-viewport"; eraseAck: TerminalOutputAck; }>; export type RasterLeaseRequest = Readonly<{ ownerId: string; rect: CellRect; erase: Readonly<{ type: "raster-erase"; bytes: Uint8Array; }>; onInvalidated?: (notice: RasterLeaseInvalidatedNotification) => void; }>; export type TerminalOutputOperation = Readonly<{ type: "generic-render"; rect: CellRect; bytes: Uint8Array; }> | Readonly<{ type: "generic-full-redraw"; rect: CellRect; bytes: Uint8Array; }> | Readonly<{ type: "raster-multipart-batch"; records: readonly Uint8Array[]; prefix?: Uint8Array; afterPrefix?: () => Promise; /** Synchronous freshness gate evaluated immediately before terminal output. */ shouldWrite?: () => boolean; replayPrefix?: Uint8Array; suffix?: Uint8Array; abortSuffix?: Uint8Array; restoreCursorVisibility?: boolean; }> | Readonly<{ type: "raster-erase"; bytes: Uint8Array; }> | Readonly<{ type: "raster-probe"; bytes: Uint8Array; }> | Readonly<{ type: "queued-output"; bytes: Uint8Array; shouldWrite?: () => boolean; /** Runs synchronously at the terminal write boundary after a successful write. */ onWritten?: () => void; }>; export type TerminalOutputAck = Readonly<{ queueId: number; operation: TerminalOutputOperation["type"]; status: "written" | "stale-token" | "revoked" | "failed"; token?: RasterLeaseToken; }>; export type LifecycleCleanupAck = Readonly<{ attempted: number; written: number; stillPending: number; }>; export type RasterLeaseAcquireResult = Readonly<{ status: "acquired"; token: RasterLeaseToken; }> | Readonly<{ status: "rejected"; reason: "invalid-geometry" | "terminal-unavailable" | "owner-conflict" | "manual-viewport"; }>; /** Discrete mouse-wheel notch size in terminal rows (xterm/less-style). */ export declare const DEFAULT_WHEEL_LINES = 3; /** * Repeat clicks on the same cell within this window escalate char -> word -> line * selection. SGR mouse reports carry no click counter, so the escalation is timed * here the way a terminal emulator would time its own native selection. */ export declare const DEFAULT_MULTI_CLICK_INTERVAL_MS = 400; type InputListenerResult = { consume?: boolean; data?: string; } | undefined; type InputListener = (data: string) => InputListenerResult; type PostRenderEmission = { payload: string; onWritten?: () => void; }; /** * Component interface - all components must implement this */ export type MouseEvent = { kind: "wheel" | "click" | "drag" | "release"; direction?: -1 | 1; button?: 0; /** Terminal cell coordinates, one-based. */ x: number; y: number; /** Focused-overlay cell coordinates, one-based when dispatched to an overlay. */ localX?: number; localY?: number; }; type OverlayMouseBounds = { row: number; col: number; width: number; height: number; termWidth: number; termHeight: number; }; export type MouseSelectionPoint = { line: number; column: number; }; /** Parse xterm SGR mouse reports for wheel, left-click, drag, and release events. */ export declare function parseSgrMouseEvent(data: string): MouseEvent | undefined; export interface Component { /** * Render the component to lines for the given viewport width * @param width - Current viewport width * @returns Array of strings, each representing a line */ render(width: number): string[]; /** * Optional handler for keyboard input when component has focus */ handleInput?(data: string): void; /** Optional handler for terminal mouse events when component has focus. */ handleMouse?(event: MouseEvent): void; /** * If true, component receives key release events (Kitty protocol). * Default is false - release events are filtered out. */ wantsKeyRelease?: boolean; /** * Optional monotonic revision for renderer-level subtree reuse. Components that * expose this MUST advance it whenever render output can change without a * width change. Unversioned components are always rendered normally. */ getRenderRevision?(): bigint; /** * Invalidate any cached rendering state. * Called when theme changes or when component needs to re-render from scratch. */ invalidate(): void; /** * Optional cleanup hook. Called once when the component is permanently * removed from the tree via removeChild/clear/dispose. Implementations MUST * be idempotent. Components meant to be re-added should be detached, not * removed/cleared. */ dispose?(): void; } /** * Interface for components that can receive focus and display a hardware cursor. * When focused, the component should emit CURSOR_MARKER at the cursor position * in its render output. TUI will find this marker and position the hardware * cursor there for proper IME candidate window positioning. */ export interface Focusable { /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */ focused: boolean; } /** Type guard to check if a component implements Focusable */ export declare function isFocusable(component: Component | null): component is Component & Focusable; /** * Cursor position marker - APC (Application Program Command) sequence. * This is a zero-width escape sequence that terminals ignore. * Components emit this at the cursor position when focused. * TUI finds and strips this marker, then positions the hardware cursor there. */ export declare const CURSOR_MARKER = "\u001B_pi:c\u0007"; export { visibleWidth }; /** Durable source identifier for a semantically anchored viewport row. */ export type ViewportAnchorId = string; /** Immutable renderer-owned details of the most recently rendered viewport. */ export interface TuiViewportObservation { transcriptCapacity: number; pinBoundary: { row: number; pinned: boolean; }; manualHistory: boolean; newOutputNoticeVisible: boolean; outputRevision: string | null; focused: boolean; cursor: { row: number; col: number; visible: boolean; } | null; selection: { start: MouseSelectionPoint; end: MouseSelectionPoint; } | null; semanticAnchor: (ViewportAnchorRow & { frameRow: number; }) | null; } export interface ViewportAnchorRow { id: ViewportAnchorId; graphemeStart: number; graphemeEnd: number; cellStart: number; cellEnd: number; } export interface ViewportAnchorRender { lines: string[]; anchors: Array; } export interface ViewportAnchorProvider extends Component { renderWithViewportAnchors(width: number): ViewportAnchorRender; } export interface ViewportAnchorSource { id: ViewportAnchorId; } /** Identity and monotonic revision of the logical output producer. */ export type ViewportOutputSource = { identity: string; revision: bigint; }; export interface ViewportAnchorSourceRenderer extends Component { renderWithViewportAnchorSource(width: number, source: ViewportAnchorSource): ViewportAnchorRender; } export declare function isViewportAnchorProvider(component: Component): component is ViewportAnchorProvider; export declare function isViewportAnchorSourceRenderer(component: Component): component is ViewportAnchorSourceRenderer; export declare function renderComponentWithViewportAnchors(component: Component, width: number): ViewportAnchorRender; export declare function renderComponentWithViewportAnchorSource(component: Component, width: number, source: ViewportAnchorSource): ViewportAnchorRender; /** * Anchor position for overlays */ export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center" | "left-center" | "right-center"; /** * Margin configuration for overlays */ export interface OverlayMargin { top?: number; right?: number; bottom?: number; left?: number; } /** Value that can be absolute (number) or percentage (string like "50%") */ export type SizeValue = number | `${number}%`; /** * Startup sixel capability probe policy (pure; exported for tests): * - Never probe when PI_FORCE_IMAGE_PROTOCOL is set — an explicit * configuration (including "off") is authoritative. * - Never probe inside a terminal multiplexer: tmux advertises DA1 ";4" * whenever it was compiled with sixel support, regardless of whether the * attached client terminal can render sixel, so a positive reply is not * end-to-end evidence. Graphics under a multiplexer are strictly opt-in * via PI_FORCE_IMAGE_PROTOCOL=sixel. * - Probe Windows Terminal (>=1.22 renders sixel but exposes no env marker). */ export declare function shouldProbeSixelCapability(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean; /** * True when repainting only the live viewport is safer than clearing/replaying * the full transcript. Real process terminals are viewport-sensitive because * their native scrollback position is not observable by the renderer. Native * Windows console hosts are also recognized from platform identity when that * process-terminal capability is unavailable. */ export declare function shouldUseViewportRepaintForHost(env?: Record, platform?: NodeJS.Platform, options?: { includeNativeWindows?: boolean; includeProcessTerminal?: boolean; }): boolean; /** * Viewport-repaint host gate resolved against a terminal's reported capability. * * `includeNativeWindows` exists so a Windows console host that cannot report * `isProcessTerminal` is still recognized from platform identity. It is a * fallback, so it must not outrank a terminal that has answered: a terminal * reporting `false` is not a native console host, and letting win32 override it * gives every non-process terminal on Windows — embedders, pipes, and the * render regression suite — viewport-repaint semantics. Those hosts then never * replay durable history, so contracted rows survive as duplicates. */ export declare function shouldUseViewportRepaintForTerminal(isProcessTerminal: boolean | undefined, env?: Record, platform?: NodeJS.Platform): boolean; /** * Options for overlay positioning and sizing. * Values can be absolute numbers or percentage strings (e.g., "50%"). */ export interface OverlayOptions { /** Width in columns, or percentage of terminal width (e.g., "50%") */ width?: SizeValue; /** Minimum width in columns */ minWidth?: number; /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */ maxHeight?: SizeValue; /** Anchor point for positioning (default: 'center') */ anchor?: OverlayAnchor; /** Horizontal offset from anchor position (positive = right) */ offsetX?: number; /** Vertical offset from anchor position (positive = down) */ offsetY?: number; /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */ row?: SizeValue; /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */ col?: SizeValue; /** Margin from terminal edges. Number applies to all sides. */ margin?: OverlayMargin | number; /** * Control overlay visibility based on terminal dimensions. * If provided, overlay is only rendered when this returns true. * Called each render cycle with current terminal dimensions. */ visible?: (termWidth: number, termHeight: number) => boolean; } /** * Handle returned by showOverlay for controlling the overlay */ export interface OverlayHandle { /** Permanently remove the overlay (cannot be shown again) */ hide(): void; /** Temporarily hide or show the overlay */ setHidden(hidden: boolean): void; /** Check if overlay is temporarily hidden */ isHidden(): boolean; } /** * Container - a component that contains other components */ export declare class Container implements ViewportAnchorProvider { #private; children: Component[]; getRenderRevision(): bigint; addChild(component: Component): void; removeChild(component: Component): void; /** Remove a child without disposing it (for detach-then-readd reuse). */ detachChild(component: Component): void; /** * Non-disposing parentage query: is `component` still a live direct child? * * Callers that park a component here and later move it elsewhere must ask * this instead of consulting their own bookkeeping — `clear()`/`dispose()` * tear children down without notifying anyone. Disposal is terminal: a * disposed child, or a child of a disposed container, is never live. */ hasLiveChild(component: Component): boolean; clear(): void; /** Remove all children without disposing them (for detach-then-readd reuse). */ detachAll(): void; /** Replace direct children without disposing reusable components. */ replaceChildren(children: Component[]): void; /** Registers a direct child as eligible for semantic viewport anchoring. */ setViewportAnchorSource(component: Component, source: ViewportAnchorSource | null): void; dispose(): void; invalidate(): void; render(width: number): string[]; renderWithViewportAnchors(width: number): ViewportAnchorRender; } type TuiRenderCounterSnapshot = { debugRedrawEnvReads: number; debugRedrawAppendWrites: number; differentialGuardVisibleWidthCalls: number; }; /** * TUI - Main class for managing terminal UI with differential rendering */ export declare class TUI extends Container { #private; private readonly options; terminal: Terminal; /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */ onDebug?: () => void; static resetRenderCountersForTest(): void; static getRenderCountersForTest(): TuiRenderCounterSnapshot; overlayStack: { component: Component; options?: OverlayOptions; preFocus: Component | null; hidden: boolean; mouseBounds?: OverlayMouseBounds; }[]; constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: { enableMouse?: boolean; copySelection?: (text: string) => void | Promise; /** * Trailing debounce for the settled width repair, in ms. `0` disables the * settled repair (deterministic harnesses need this — a wall-clock-timed * full replay lands at nondeterministic logical positions). Defaults to * `VIB_TUI_WIDTH_SETTLE_MS` / `PI_TUI_WIDTH_SETTLE_MS`, then 1000. */ widthSettleMs?: number; /** * Window in ms for repeat clicks on one cell to escalate to word/line * selection. Defaults to `DEFAULT_MULTI_CLICK_INTERVAL_MS`; `0` disables * escalation so every click starts a fresh character selection. */ multiClickIntervalMs?: number; }); dispose(): void; get fullRedraws(): number; getShowHardwareCursor(): boolean; setShowHardwareCursor(enabled: boolean): void; getClearOnShrink(): boolean; setClearOnShrink(enabled: boolean): void; setFocus(component: Component | null): void; /** Returns the currently focused component without exposing mutable focus state. */ getFocusedComponent(): Component | null; /** Returns a defensive snapshot of the latest renderer-owned viewport anchors. */ getViewportAnchorSnapshot(): { startRow: number; anchors: Array; } | null; /** Returns a defensive snapshot of renderer-owned viewport geometry. */ getViewportObservation(): TuiViewportObservation | null; /** Selects a zero-based painted viewport cell range using the same renderer path as mouse dragging. */ setViewportSelection(start: MouseSelectionPoint, end: MouseSelectionPoint): void; removeChild(component: Component): void; clear(): void; setBottomPinnedComponent(component: Component | null): void; /** Report the logical output producer revision without coupling TUI to message types. */ setViewportOutputSource(source: ViewportOutputSource | null): void; /** Register the direct child whose rows are eligible for semantic viewport anchoring. */ setViewportAnchorComponent(component: Component | null): void; /** Returns the direct component registered as the semantic viewport anchor source. */ getViewportAnchorComponent(): Component | null; /** Clear manual viewport ownership and durable history before replacing the transcript identity. */ resetViewportAnchorIntent(): void; /** Allow one semantic-neighbor reconciliation after a definitive same-transcript rebuild. */ prepareViewportAnchorForTranscriptRebuild(): void; /** Reveal a semantic viewport anchor without changing the rendered content width. */ revealViewportAnchor(id: ViewportAnchorId, alignment: "top" | "center" | "bottom"): boolean; scrollViewportBy(deltaRows: number, options?: { /** edge: PageUp/PageDown pin; stable: preserve/center pin for fine wheel motion */ pin?: "edge" | "stable"; }): boolean; scrollViewportPages(direction: -1 | 1): boolean; followLiveViewport(): boolean; /** * Show an overlay component with configurable positioning and sizing. * Returns a handle to control the overlay's visibility. */ showOverlay(component: Component, options?: OverlayOptions): OverlayHandle; /** Hide the topmost overlay and restore previous focus. */ hideOverlay(): void; /** Check if there are any visible overlays */ hasOverlay(): boolean; invalidate(): void; acquireRasterLease(request: RasterLeaseRequest): Promise; submitTerminalOutput(request: Readonly<{ operation: TerminalOutputOperation; token?: RasterLeaseToken; }>): Promise; invalidateRasterLease(request: Readonly<{ token: RasterLeaseToken; cause: RasterLeaseInvalidatedNotification["cause"]; }>): Promise; notifyTerminalLifecycle(event: { kind: "availability-restored" | "explicit-cleanup"; source: "tui" | "interactive-mode" | "transport"; terminalGeneration: number; }): Promise; start(): void; /** * Wait for a specific render request generation to be written successfully. * * Render requests are coalesced, so committing a newer generation also commits * every older generation represented by that frame. A stopped or unavailable * terminal resolves waiters false so UI callers can fail open instead of * holding a session operation behind a dead renderer. */ waitForRenderCommit(generation: number, timeoutMs?: number): Promise; get terminalAvailable(): boolean; get isRunning(): boolean; get terminalGeneration(): number; get manualViewportActive(): boolean; addInputListener(listener: InputListener): () => void; drainInput(maxMs: number, quiescenceMs: number): Promise; drainPetProbeInput(maxMs: number, quiescenceMs: number): Promise; removeInputListener(listener: InputListener): void; /** Refresh terminal cell metrics for a verified external image transport. */ refreshImageCellSize(): void; stop(): void; /** * Viewport-repaint-aware resize render request. * * A forced repaint resets `#previousWidth`/`#previousHeight` to -1, which makes * `#doRender` treat the frame as a dimension change. Repaints stay anchored to * the live viewport so native scrollback is never replayed or erased. * * Spurious resize events (SIGWINCH with unchanged dimensions — iTerm2 tab * switches and window focus changes, the self-sent SIGWINCH after resume) * must not force either: only force when the grid size actually changed since * the last committed frame. */ requestResizeRender(): void; requestRender(force?: boolean, source?: string): void; /** Request a frame whose mutation is known to be outside the viewport-anchor subtree. */ requestLayoutRender(source?: string): void; requestRenderWithGeneration(force?: boolean, source?: string): number; getLineRenderCacheStats(): { normalizationSize: number; truncationSize: number; normalizationLimit: number; truncationLimit: number; }; /** Retain terminal cleanup until a write succeeds, even after its component is disposed. */ queueTerminalCleanup(payload: string, onDelivered?: () => void): Promise; /** Queue protocol-neutral output behind the same terminal ordering as renders. */ queueTerminalOutput(payload: string, options?: { shouldWrite?: () => boolean; onWritten?: () => void; }): Promise; /** Retry retained cleanup after recovery or before shutdown. */ flushTerminalCleanup(restoreTerminalAvailability?: boolean): void; /** * Register an emitter whose payload is delivered after each shared render * transaction. The emitter is an exempt physical overlay: its bytes are * deliberately kept out of the shared transcript write. */ setPostRenderEmitter(emitter: (() => string | PostRenderEmission | null) | undefined): void; }