@opentui/core
Version:
OpenTUI is a TypeScript library on a native Zig core for building terminal user interfaces (TUIs)
622 lines (621 loc) • 23.2 kB
TypeScript
import { Renderable, RootRenderable } from "./Renderable.js";
import { DebugOverlayCorner, type CursorStyleOptions, type MousePointerStyle, type RenderContext, type TerminalCapabilities, type ThemeMode, type SelectionBehavior, type WidthMethod } from "./types.js";
import { RGBA, type ColorInput } from "./lib/RGBA.js";
import { OptimizedBuffer } from "./buffer.js";
import { type NativeBufferedOutput, type NativeRenderStats, type RendererHandle } from "./zig.js";
import { TerminalConsole, type ConsoleOptions } from "./console.js";
import { type MouseEventType, type RawMouseEvent, type ScrollInfo } from "./lib/parse.mouse.js";
import { Selection } from "./lib/selection.js";
import { type ClipboardTarget } from "./lib/clipboard.js";
import { EventEmitter } from "events";
import { KeyHandler, InternalKeyHandler } from "./lib/KeyHandler.js";
import { type EditBufferRenderable } from "./renderables/EditBufferRenderable.js";
import { type TerminalColors, type GetPaletteOptions } from "./lib/terminal-palette.js";
import { type Clock } from "./lib/clock.js";
export interface CliRendererConfig {
stdin?: NodeJS.ReadStream;
stdout?: NodeJS.WriteStream;
width?: number;
height?: number;
remote?: boolean;
bufferedOutput?: NativeBufferedOutput;
exitOnCtrlC?: boolean;
exitSignals?: NodeJS.Signals[];
clearOnShutdown?: boolean;
forwardEnvKeys?: string[];
debounceDelay?: number;
targetFps?: number;
maxFps?: number;
memorySnapshotInterval?: number;
useThread?: boolean;
gatherStats?: boolean;
maxStatSamples?: number;
consoleOptions?: Omit<ConsoleOptions, "clock">;
postProcessFns?: ((buffer: OptimizedBuffer, deltaTime: number) => void)[];
enableMouseMovement?: boolean;
useMouse?: boolean;
autoFocus?: boolean;
screenMode?: ScreenMode;
footerHeight?: number;
externalOutputMode?: ExternalOutputMode;
consoleMode?: ConsoleMode;
useKittyKeyboard?: KittyKeyboardOptions | null;
backgroundColor?: ColorInput;
openConsoleOnError?: boolean;
prependInputHandlers?: ((sequence: string) => boolean)[];
stdinParserMaxBufferBytes?: number;
clock?: Clock;
onDestroy?: () => void;
}
export type ScreenMode = "alternate-screen" | "main-screen" | "split-footer";
export type ExternalOutputMode = "capture-stdout" | "passthrough";
export interface CliRendererExternalOutputEvent {
snapshot: OptimizedBuffer;
rowColumns: number;
startOnNewLine: boolean;
trailingNewline: boolean;
}
export type ConsoleMode = "console-overlay" | "disabled";
export type PixelResolution = {
width: number;
height: number;
};
export interface CliRendererStats extends NativeRenderStats {
fps: number;
frameCount: number;
frameTimes: number[];
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
frameCallbackTime: number;
}
export interface CliRendererFrameEvent {
frameId: number;
}
export interface CliRendererErrorEvent {
error: Error;
renderable: Renderable | undefined;
}
export interface RendererSchedulerState {
isRunning: boolean;
isRendering: boolean;
hasScheduledRender: boolean;
}
export interface ScrollbackRenderContext {
width: number;
widthMethod: WidthMethod;
tailColumn: number;
renderContext: RenderContext;
}
export interface ScrollbackSnapshot {
root: Renderable;
width?: number;
height?: number;
rowColumns?: number;
startOnNewLine?: boolean;
trailingNewline?: boolean;
teardown?: () => void;
}
export type ScrollbackWriter = (ctx: ScrollbackRenderContext) => ScrollbackSnapshot;
export interface ScrollbackSurfaceOptions {
startOnNewLine?: boolean;
}
export interface ScrollbackSurfaceCommitOptions {
rowColumns?: number;
trailingNewline?: boolean;
}
export interface ScrollbackSurface {
readonly renderContext: RenderContext;
readonly root: Renderable;
readonly width: number;
readonly height: number;
readonly isDestroyed: boolean;
render(): void;
settle(timeoutMs?: number): Promise<void>;
commitRows(startRow: number, endRowExclusive: number, options?: ScrollbackSurfaceCommitOptions): void;
destroy(): void;
}
export interface SplitFooterReplayResetOptions {
clearSavedLines?: boolean;
}
/**
* Kitty Keyboard Protocol configuration options
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
*/
export interface KittyKeyboardOptions {
/** Disambiguate escape codes (fixes ESC timing, alt+key ambiguity, ctrl+c as event). Default: true */
disambiguate?: boolean;
/** Report alternate keys (numpad, shifted, base layout) for cross-keyboard shortcuts. Default: true */
alternateKeys?: boolean;
/** Report event types (press/repeat/release). Default: false */
events?: boolean;
/** Report all keys as escape codes. Default: false */
allKeysAsEscapes?: boolean;
/** Report text associated with key events. Default: false */
reportText?: boolean;
}
/**
* Build kitty keyboard protocol flags based on configuration
* @param config Kitty keyboard configuration object (null/undefined = disabled)
* @returns The combined flags value (0 = disabled, >0 = enabled)
* @internal Exported for testing
*/
export declare function buildKittyKeyboardFlags(config: KittyKeyboardOptions | null | undefined): number;
export declare class MouseEvent {
readonly type: MouseEventType;
readonly button: number;
readonly x: number;
readonly y: number;
readonly source?: Renderable;
readonly modifiers: {
shift: boolean;
alt: boolean;
ctrl: boolean;
};
readonly scroll?: ScrollInfo;
readonly target: Renderable | null;
readonly currentTarget: Renderable | null;
readonly isDragging?: boolean;
private _propagationStopped;
private _defaultPrevented;
get propagationStopped(): boolean;
get defaultPrevented(): boolean;
constructor(target: Renderable | null, attributes: RawMouseEvent & {
source?: Renderable;
isDragging?: boolean;
});
stopPropagation(): void;
preventDefault(): void;
}
export interface CliRendererHandlerErrorEvent {
error: unknown;
event: MouseEvent;
}
export declare enum MouseButton {
LEFT = 0,
MIDDLE = 1,
RIGHT = 2,
WHEEL_UP = 4,
WHEEL_DOWN = 5
}
/**
* Create a CLI renderer and run its async terminal setup. The constructor
* owns all stream and backend decisions; this factory only layers on the
* `--delay-start` flag and the `await setupTerminal()` convenience.
*/
export declare function createCliRenderer(config?: CliRendererConfig): Promise<CliRenderer>;
export declare enum CliRenderEvents {
RESIZE = "resize",
FRAME = "frame",
RENDER_ERROR = "render:error",
HANDLER_ERROR = "handler:error",
EXTERNAL_OUTPUT = "external_output",
FOCUS = "focus",
BLUR = "blur",
FOCUSED_RENDERABLE = "focused_renderable",
FOCUSED_EDITOR = "focused_editor",
THEME_MODE = "theme_mode",
PALETTE = "palette",
CAPABILITIES = "capabilities",
SELECTION = "selection",
DEBUG_OVERLAY_TOGGLE = "debugOverlay:toggle",
DESTROY = "destroy",
MEMORY_SNAPSHOT = "memory:snapshot"
}
export declare enum RendererControlState {
IDLE = "idle",
AUTO_STARTED = "auto_started",
EXPLICIT_STARTED = "explicit_started",
EXPLICIT_PAUSED = "explicit_paused",
EXPLICIT_SUSPENDED = "explicit_suspended",
EXPLICIT_STOPPED = "explicit_stopped"
}
export declare class CliRenderer extends EventEmitter implements RenderContext {
private static animationFrameId;
private lib;
rendererPtr: RendererHandle;
stdin: NodeJS.ReadStream;
private stdout;
private exitOnCtrlC;
private exitSignals;
private _exitListenersAdded;
private _isDestroyed;
private _destroyPending;
private _destroyFinalized;
private _destroyCleanupPrepared;
private _streamLeaseAcquired;
nextRenderBuffer: OptimizedBuffer;
currentRenderBuffer: OptimizedBuffer;
private _isRunning;
private _targetFps;
private _maxFps;
private automaticMemorySnapshot;
private memorySnapshotInterval;
private memorySnapshotTimer;
private lastMemorySnapshot;
readonly root: RootRenderable;
width: number;
height: number;
private _useThread;
private gatherStats;
private frameTimes;
private maxStatSamples;
private postProcessFns;
private backgroundColor;
private waitingForPixelResolution;
private pixelResolutionRequeryPending;
private readonly clock;
private rendering;
private renderingNative;
private renderTimeout;
private lastTime;
private frameCount;
private _frameId;
private lastFpsTime;
private currentFps;
private targetFrameTime;
private minTargetFrameTime;
private immediateRerenderRequested;
private updateScheduled;
private liveRequestCounter;
private _controlState;
private frameCallbacks;
private renderStats;
debugOverlay: {
enabled: any;
corner: DebugOverlayCorner;
};
private _console;
private _resolution;
private _keyHandler;
private stdinParser;
private readonly oscSubscribers;
private hasLoggedStdinParserError;
private animationRequest;
private resizeTimeoutId;
private capabilityTimeoutId;
private terminalKeepAliveTimer;
private xtVersionWaiters;
private splitStartupSeedTimeoutId;
private pendingSplitStartupCursorSeed;
private resizeDebounceDelay;
private enableMouseMovement;
private _useMouse;
private autoFocus;
private _screenMode;
private _footerHeight;
private _externalOutputMode;
private clearOnShutdown;
private _suspendedMouseEnabled;
private _previousControlState;
private pendingSuspendedTerminalSetup;
private suspendedNonAltSurfacePreserved;
private capturedRenderable?;
private lastOverRenderableNum;
private lastOverRenderable?;
private currentSelection;
private selectionContainers;
private lastClick;
private clipboard;
private _splitHeight;
private renderOffset;
private splitTailColumn;
private pendingSplitFooterTransition;
private forceFullRepaintRequested;
private readonly maxSplitCommitsPerFrame;
private _terminalWidth;
private _terminalHeight;
private _terminalIsSetup;
private externalOutputQueue;
private pendingExternalOutputMode;
private realStdoutWrite;
private _useConsole;
private sigwinchHandler;
private _capabilities;
private _latestPointer;
private _hasPointer;
private _lastPointerModifiers;
private _currentMousePointerStyle;
private _currentFocusedRenderable;
private lifecyclePasses;
private _openConsoleOnError;
private _paletteDetector;
private _paletteCache;
private _paletteDetectionPromise;
private _paletteDetectionSize;
private _paletteEpoch;
private _nativePaletteSignature;
private _emittedPaletteSignature;
private _palettePublishGeneration;
private _onDestroy?;
private themeModeState;
private _terminalFocusState;
private sequenceHandlers;
private prependedInputHandlers;
private shouldRestoreModesOnNextFocus;
private themeModeHandler;
private idleResolvers;
private _debugInputs;
private _debugModeEnabled;
private readonly stdinLogPath;
private handleError;
private dumpOutputCache;
private exitHandler;
private warningHandler;
private readonly _usesProcessStdout;
private _feed;
private _detachFeed;
private _detachFeedError;
private feedIdleRenderScheduled;
private ordinaryFrameWaitingForFeed;
private ordinaryFrameWaitControlState;
get controlState(): RendererControlState;
/**
* Construct a renderer over the given streams.
*
* If `stdout` is not `process.stdout`, a `NativeSpanFeed` is allocated
* internally and rendered bytes are piped through it to `stdout` unless
* `bufferedOutput: "memory"` is set. Prefer `createCliRenderer` for the async
* `setupTerminal` convenience.
*
* Construction side effects (observable before the constructor returns):
* - Acquires exclusive ownership of the given stdin/stdout streams
* - Allocates a `NativeSpanFeed` (for non-process stdout unless bufferedOutput is "memory")
* - Calls `lib.createRenderer` → native Zig allocation
* - Registers in the process-wide `rendererTracker`
* - Adds `process.on(...)` listeners for SIGWINCH (process.stdout only),
* "warning", "uncaughtException", "unhandledRejection", plus the
* configured `exitSignals`
* - Replaces `global.requestAnimationFrame` with the renderer's impl
* - When `setupTerminal()` is called, it will put `stdin` in raw mode and
* call `stdin.resume()`
*
* Some late constructor side effects are not rolled back if construction
* throws partway; production callers should use `createCliRenderer`, which
* wraps `setupTerminal()` in a try/catch that calls `destroy()` on failure.
*/
constructor(stdin: NodeJS.ReadStream, stdout: NodeJS.WriteStream, width: number, height: number, config?: CliRendererConfig);
private addExitListeners;
private startTerminalKeepAlive;
private stopTerminalKeepAlive;
private removeExitListeners;
get isDestroyed(): boolean;
registerLifecyclePass(renderable: Renderable): void;
unregisterLifecyclePass(renderable: Renderable): void;
getLifecyclePasses(): Set<Renderable>;
get currentFocusedRenderable(): Renderable | null;
get currentFocusedEditor(): EditBufferRenderable | null;
private normalizeClockTime;
private getElapsedMs;
focusRenderable(renderable: Renderable): void;
blurRenderable(renderable: Renderable): void;
private setCapturedRenderable;
addToHitGrid(x: number, y: number, width: number, height: number, id: number): void;
pushHitGridScissorRect(x: number, y: number, width: number, height: number): void;
popHitGridScissorRect(): void;
clearHitGridScissorRects(): void;
get widthMethod(): WidthMethod;
get frameId(): number;
private writeOut;
private scheduleRenderAfterFeedIdle;
private handleNativeRenderRejection;
private reportNativeRenderFailure;
private scheduleRenderTimer;
private scheduleRenderAfterBackpressure;
requestRender(): void;
private activateFrame;
get consoleMode(): ConsoleMode;
set consoleMode(mode: ConsoleMode);
get isRunning(): boolean;
private isIdleNow;
private resolveIdleIfNeeded;
idle(): Promise<void>;
getSchedulerState(): RendererSchedulerState;
get resolution(): PixelResolution | null;
get console(): TerminalConsole;
get keyInput(): KeyHandler;
get _internalKeyInput(): InternalKeyHandler;
get terminalWidth(): number;
get terminalHeight(): number;
get useThread(): boolean;
get targetFps(): number;
set targetFps(targetFps: number);
get maxFps(): number;
set maxFps(maxFps: number);
get useMouse(): boolean;
set useMouse(useMouse: boolean);
get screenMode(): ScreenMode;
set screenMode(mode: ScreenMode);
get footerHeight(): number;
set footerHeight(footerHeight: number);
get externalOutputMode(): ExternalOutputMode;
set externalOutputMode(mode: ExternalOutputMode);
private applyExternalOutputMode;
private afterExternalOutputModeChanged;
private applyPendingExternalOutputModeIfReady;
private flushPendingSplitOutputBeforeLeavingSplitFooter;
get liveRequestCount(): number;
get currentControlState(): string;
get capabilities(): TerminalCapabilities | null;
triggerNotification(message: string, title?: string): boolean;
get themeMode(): ThemeMode | null;
waitForThemeMode(timeoutMs?: number): Promise<ThemeMode | null>;
getDebugInputs(): Array<{
timestamp: string;
sequence: string;
}>;
get useKittyKeyboard(): boolean;
set useKittyKeyboard(use: boolean);
createScrollbackSurface(options?: ScrollbackSurfaceOptions): ScrollbackSurface;
writeToScrollback(write: ScrollbackWriter): void;
resetSplitFooterForReplay(options?: SplitFooterReplayResetOptions): void;
private getSnapshotWidth;
private getSnapshotHeight;
private getSnapshotRowWidths;
private advanceSplitTailColumn;
private getSplitTailColumnAfterCommit;
private recordSplitCommit;
private getPendingSplitTailColumn;
private enqueueRenderedScrollbackCommit;
private enqueueSplitCommit;
private createStdoutSnapshotCommit;
private splitStdoutRows;
private createStdoutSnapshotCommits;
private flushPendingSplitCommits;
private interceptStdoutWrite;
private getSplitPinnedRenderOffset;
private getSplitCursorSeedRows;
private isSplitCursorSeedFrameBlocked;
private canFlushSplitOutputBeforeTransition;
private clearSplitStartupCursorSeed;
private abortSplitStartupCursorSeed;
private flushPendingSplitOutputBeforeTransition;
private resetSplitScrollback;
private syncSplitScrollback;
private getSplitOutputOffset;
private clearPendingSplitFooterTransition;
private setPendingSplitFooterTransition;
private syncSplitFooterState;
private clearStaleSplitSurfaceRows;
private applyScreenMode;
private flushStdoutCache;
private enableMouse;
private disableMouse;
enableKittyKeyboard(flags?: number): void;
disableKittyKeyboard(): void;
set useThread(useThread: boolean);
setupTerminal(): Promise<void>;
private stdinListener;
addInputHandler(handler: (sequence: string) => boolean): void;
prependInputHandler(handler: (sequence: string) => boolean): void;
removeInputHandler(handler: (sequence: string) => boolean): void;
private updateStdinParserProtocolContext;
subscribeOsc(handler: (sequence: string) => void): () => void;
private processCapabilitySequence;
private capabilityHandler;
private focusHandler;
private dispatchSequenceHandlers;
private drainStdinParser;
private handleStdinEvent;
private handleStdinParserFailure;
private setupInput;
private dispatchMouseEvent;
private sendMouseEvent;
private processSingleMouseEvent;
/**
* Recheck hover state after hit grid changes.
* Called after render when native code detects the hit grid changed.
* Fires out/over events if the element under the cursor changed.
*/
private recheckHoverState;
setMousePointer(style: MousePointerStyle): void;
hitTest(x: number, y: number): number;
private takeMemorySnapshot;
private startMemorySnapshotTimer;
private stopMemorySnapshotTimer;
setMemorySnapshotInterval(interval: number): void;
private handleResize;
private queryPixelResolution;
private processResize;
/**
* Programmatically resize the renderer to new dimensions.
*
* Use this for externally-driven resize events — for example, an SSH
* `window-change` signal or a test harness simulating a terminal resize.
* When the renderer is attached to `process.stdout`, `SIGWINCH` is handled
* automatically and callers do not need this method.
*/
resize(width: number, height: number): void;
setBackgroundColor(color: ColorInput): void;
toggleDebugOverlay(): void;
configureDebugOverlay(options: {
enabled?: boolean;
corner?: DebugOverlayCorner;
}): void;
setTerminalTitle(title: string): void;
/**
* Reset the terminal background color to its default via OSC 111.
* Called automatically by destroy() and suspend(), but exposed for
* consumers that need explicit control (e.g. before SIGTSTP).
*/
resetTerminalBgColor(): void;
copyToClipboardOSC52(text: string, target?: ClipboardTarget): boolean;
clearClipboardOSC52(target?: ClipboardTarget): boolean;
isOsc52Supported(): boolean;
dumpHitGrid(): void;
dumpBuffers(timestamp?: number): void;
dumpOutputBuffer(timestamp?: number): void;
static setCursorPosition(renderer: CliRenderer, x: number, y: number, visible?: boolean): void;
static setCursorStyle(renderer: CliRenderer, options: CursorStyleOptions): void;
static setCursorColor(renderer: CliRenderer, color: RGBA): void;
setCursorPosition(x: number, y: number, visible?: boolean): void;
setCursorStyle(options: CursorStyleOptions): void;
setCursorColor(color: RGBA): void;
getCursorState(): import("./zig.js").CursorState;
addPostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void;
removePostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void;
clearPostProcessFns(): void;
setFrameCallback(callback: (deltaTime: number) => Promise<void>): void;
removeFrameCallback(callback: (deltaTime: number) => Promise<void>): void;
clearFrameCallbacks(): void;
requestLive(): void;
dropLive(): void;
start(): void;
auto(): void;
private internalStart;
pause(): void;
suspend(): void;
resume(): void;
private internalPause;
stop(): void;
private internalStop;
destroy(): void;
private cleanupBeforeDestroy;
private prepareDestroyDuringRender;
private finalizeDestroy;
private startRenderLoop;
private loop;
intermediateRender(): void;
private renderNative;
private collectStatSample;
getNativeStats(): NativeRenderStats;
getStats(): CliRendererStats;
resetStats(): void;
setGatherStats(enabled: boolean): void;
getSelection(): Selection | null;
get hasSelection(): boolean;
getSelectionContainer(): Renderable | null;
clearSelection(): void;
private clearSelectionState;
/**
* Start a new selection at the given coordinates.
* Used by both mouse and keyboard selection.
*/
startSelection(renderable: Renderable, x: number, y: number, behavior?: SelectionBehavior): void;
private nextClickBehavior;
updateSelection(currentRenderable: Renderable | undefined, x: number, y: number, options?: {
finishDragging?: boolean;
}): void;
requestSelectionUpdate(): void;
private isWithinContainer;
private finishSelection;
private notifySelectablesOfSelectionChange;
private walkSelectableRenderables;
get paletteDetectionStatus(): "idle" | "detecting" | "cached";
private getCachedPaletteBySize;
private ensurePaletteDetector;
private syncNativePaletteState;
private emitPaletteChange;
private resolveXtVersionWaiters;
private waitForXtVersion;
private shouldSyncNativePaletteState;
private refreshPalette;
clearPaletteCache(): void;
/**
* Detects the terminal's color palette
*
* @returns Promise resolving to TerminalColors object containing palette and special colors
* @throws Error if renderer is suspended
*/
getPalette(options?: GetPaletteOptions): Promise<TerminalColors>;
}