import type { Operation } from 'effection'; import type { GhostwrightError } from './errors.ts'; export interface Viewport { columns: number; rows: number; widthPixels?: number; heightPixels?: number; } export interface CleanupOptions { hangupGraceMs?: number; terminateGraceMs?: number; postExitDrainMs?: number; } export interface HistoryOptions { maxRevisions?: number; maxRawBytes?: number; maxDecodedBytes?: number; } /** Per-active-screen Kitty image storage configuration. */ export interface GraphicsOptions { /** Decoded-image storage cap in bytes. Defaults to 64 MiB; zero disables storage. */ storageLimitBytes?: number; } export type TracePolicy = 'off' | 'retain-on-failure' | 'on'; export interface TraceOptions { policy?: TracePolicy; directory?: string; redactArgumentIndexes?: readonly number[]; } export interface RegisteredOscMessage { number: number; namespace: string; parameters: readonly string[]; payload: Uint8Array; terminator: 'ST' | 'BEL'; } /** A framework-neutral ordered OSC extension registration. */ export interface OscRegistration { number: number; namespace: string; /** Maximum bytes retained for an incomplete registered sequence. */ maxBufferedBytes: number; decode(message: RegisteredOscMessage): TCommit; } export interface ExtensionRevision { sequence: number; timestamp: number; extensionId: string; protocolFrame: number; screenSequence: number; value: T; } export interface ExtensionCommit { protocolFrame: number; value: T; } export interface ExtensionSessionContext { readonly terminal: AsyncTerminal; readonly screen: ScreenReader; publish(commit: ExtensionCommit): ExtensionRevision; diagnostic(error: GhostwrightError): void; } export interface TerminalExtensionDefinition { readonly id: string; readonly osc?: OscRegistration; createSession(context: ExtensionSessionContext): TSession; accept?(session: TSession, commit: TCommit, context: ExtensionSessionContext): void; } export interface TerminalLaunchOptions { command: string; args?: readonly string[]; cwd?: string; env?: Readonly>; viewport?: Viewport; commandTimeoutMs?: number; assertionTimeoutMs?: number; settleMs?: number; cleanup?: CleanupOptions; history?: HistoryOptions; graphics?: GraphicsOptions; trace?: TracePolicy | TraceOptions; name?: string; /** Optional framework-specific extensions receiving ordered in-band OSC commits. */ extensions?: readonly TerminalExtensionDefinition[]; } export interface Point { column: number; row: number; } export interface Rect extends Point { width: number; height: number; } export interface ActionReceipt { actionSequence: number; screenSequenceBefore: number; acknowledgedAt: number; deliveredToChild: boolean; bytesWritten: number; } /** * Default assertion timeout. * * Deliberately below the 5000 ms default of Bun, Jest, and Vitest: if the two * are equal the runner's timeout wins the race and reports a bare "timed out" * instead of Ghostwright's screen diagnostic. */ export const DEFAULT_ASSERTION_TIMEOUT_MS = 4000; export type KeyName = | 'Enter' | 'Tab' | 'Escape' | 'Backspace' | 'Delete' | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Home' | 'End' | 'PageUp' | 'PageDown' | `F${number}` | string; export interface KeyPress { key: KeyName; shift?: boolean; control?: boolean; alt?: boolean; super?: boolean; } export interface TraceableInputOptions { trace?: 'record' | 'redact'; } export interface MouseOptions { button?: 'left' | 'middle' | 'right' | number; shift?: boolean; control?: boolean; alt?: boolean; super?: boolean; } export interface WheelOptions extends Point { deltaRows: number; deltaColumns?: number; } export interface AssertionOptions { timeoutMs?: number; } export interface StableAssertionOptions extends AssertionOptions { settleMs?: number; } export interface TransientAssertionOptions extends AssertionOptions { since?: ActionReceipt | number; } export interface TextLocatorOptions { exact?: boolean; /** * Only match text whose cells all satisfy this style. Useful for * disambiguating the same string rendered in different states, such as a * focused versus unfocused label. */ style?: StyleQuery; } /** * A colour to match against. Accepts the structured {@link TerminalColor} form * or the shorthands `'#rrggbb'`, `'rgb(r,g,b)'`, `'default'`, and `'palette:N'`. */ export type ColorQuery = TerminalColor | string; /** A partial {@link CellStyle} to match against. Omitted fields are ignored. */ export interface StyleQuery { bold?: boolean; italic?: boolean; faint?: boolean; blink?: boolean; inverse?: boolean; invisible?: boolean; strikethrough?: boolean; overline?: boolean; underline?: number; foreground?: ColorQuery; background?: ColorQuery; } export interface LocatorMatch { text: string; range: Rect; rowText: string; /** The cells backing this match, in column order, for style inspection. */ cells: readonly ScreenCell[]; } export interface ProcessStatus { state: 'starting' | 'running' | 'exited' | 'closed' | 'failed'; pid?: number; processGroupId?: number; exitCode?: number | null; signal?: string | null; ptyEof: boolean; } export type TerminalColor = | { kind: 'default' } | { kind: 'palette'; index: number } | { kind: 'rgb'; red: number; green: number; blue: number }; export interface CellStyle { bold: boolean; italic: boolean; faint: boolean; blink: boolean; inverse: boolean; invisible: boolean; strikethrough: boolean; overline: boolean; underline: number; foreground: TerminalColor; background: TerminalColor; underlineColor?: TerminalColor; } export interface ScreenCell { column: number; text: string; width: 0 | 1 | 2; continuation: boolean; style: CellStyle; selected: boolean; hyperlink?: string; } export interface ScreenLine { row: number; cells: readonly ScreenCell[]; wrapped: boolean; text: string; } export interface CursorSnapshot extends Point { visible: boolean; shape: 'block' | 'bar' | 'underline'; blinking: boolean; } export interface TerminalModes { applicationCursorKeys: boolean; backarrowSendsBackspace: boolean; bracketedPaste: boolean; focusReporting: boolean; mouseTracking: 'none' | 'x10' | 'normal' | 'button' | 'any'; mouseFormat: 'x10' | 'utf8' | 'sgr' | 'urxvt' | 'sgr-pixels'; kittyKeyboardFlags: number; alternateScreen: boolean; privateModes: Readonly>; } export interface KittyImageSnapshot { id: number; number: number; generation: string; width: number; height: number; format: 'rgb' | 'rgba' | 'gray' | 'gray-alpha'; compression: 'none'; dataLength: number; sha256: string; } export interface KittyPlacementSnapshot { imageId: number; placementId: number; imageGeneration: string; image?: KittyImageSnapshot; virtual: boolean; z: number; layer: 'below-background' | 'below-text' | 'above-text'; offset: { xPixels: number; yPixels: number }; requestedGrid: { columns: number; rows: number }; renderedGrid?: { columns: number; rows: number }; renderedPixels?: { width: number; height: number }; viewport: { column?: number; row?: number; visible: boolean }; source: { x: number; y: number; width: number; height: number }; } export interface KittyGraphicsSnapshot { supported: boolean; generation: string; storageLimitBytes: number; placements: readonly KittyPlacementSnapshot[]; } export interface ScreenSnapshot { sequence: number; timestamp: number; lastVisualChangeAt: number; viewport: Required; activeBuffer: 'primary' | 'alternate'; /** Renderer-ready Kitty state for the active screen only. */ graphics: KittyGraphicsSnapshot; cursor: CursorSnapshot; lines: readonly ScreenLine[]; modes: TerminalModes; title: string; workingDirectory?: string; } export interface ScreenRevision { sequence: number; timestamp: number; cause: 'pty-output' | 'resize' | 'reset'; sourceFrameSequence?: number; changedRows: readonly number[]; visualChange: boolean; snapshot: ScreenSnapshot; } export interface RevisionRangeQuery { /** Exclusive action/revision baseline. */ since: ActionReceipt | ScreenRevision | number; /** Inclusive revision endpoint. */ until?: ScreenRevision | number; limit?: number; } export interface RevisionCollectionOptions { since: ActionReceipt | ScreenRevision | number; until(snapshot: ScreenSnapshot, revision: ScreenRevision): boolean; timeoutMs?: number; maxRevisions?: number; } export interface RevisionCollection { baselineSequence: number; startedAt: number; completedAt: number; revisions: readonly ScreenRevision[]; } export interface HistoryQuery { direction?: 'oldest-first' | 'newest-first'; /** Zero-based index from the oldest currently retained scrollback row. */ start?: number; count?: number; expectedGeneration?: string; } export interface HistoryLine extends Omit { /** Zero-based index from the oldest currently retained scrollback row. */ index: number; wrapContinuation: boolean; kittyVirtualPlaceholder: boolean; } export interface HistoryRange { generation: string; totalRows: number; start: number; direction: 'oldest-first' | 'newest-first'; lines: readonly HistoryLine[]; } export interface HistorySearchOptions { direction?: 'oldest-first' | 'newest-first'; start?: number; maxRows?: number; limit?: number; expectedGeneration?: string; } export interface HistoryMatch { lineIndex: number; text: string; range: Rect; line: HistoryLine; } export interface CellChange { point: Point; before: ScreenCell; after: ScreenCell; } export interface ScreenReader { current(): ScreenSnapshot; /** Alias of {@link ScreenReader.current}, matching `AsyncRegion.snapshot()`. */ snapshot(): ScreenSnapshot; getCell(point: Point): ScreenCell; /** Every cell inside `rect`, row-major, for style and border inspection. */ getCells(rect: Rect): readonly ScreenCell[]; getText(rect?: Rect): string; changedCells(since: ScreenSnapshot | number): readonly CellChange[]; rawOutput(): Uint8Array; /** Retained revision query. `since` is exclusive and `until` is inclusive. */ revisions(query: RevisionRangeQuery): readonly ScreenRevision[]; /** Cache-only image lookup; it never dereferences a live WASM graphics handle. */ getKittyImage(id: number): KittyImageSnapshot | undefined; scrollback(): readonly ScreenLine[]; clipboard(): string; } export interface AsyncRevisionObserver { collect(options: RevisionCollectionOptions): Promise; } export interface OperationRevisionObserver { collect(options: RevisionCollectionOptions): Operation; } export interface AsyncHistoryReader { read(query?: HistoryQuery): Promise; findText(text: string, options?: HistorySearchOptions): Promise; } export interface OperationHistoryReader { read(query?: HistoryQuery): Operation; findText(text: string, options?: HistorySearchOptions): Operation; } export interface AsyncGraphicsReader { inspectImage(id: number): Promise; copyImageData(id: number): Promise; } export interface OperationGraphicsReader { inspectImage(id: number): Operation; copyImageData(id: number): Operation; } export interface AsyncLocator { nth(index: number): AsyncLocator; region(rect: Rect): AsyncLocator; matches(): readonly LocatorMatch[]; click(options?: MouseOptions): Promise; } export interface OperationLocator { nth(index: number): OperationLocator; region(rect: Rect): OperationLocator; matches(): readonly LocatorMatch[]; click(options?: MouseOptions): Operation; } export interface AsyncRegion { getByText(text: string, options?: TextLocatorOptions): AsyncLocator; snapshot(): ScreenSnapshot; } export interface OperationRegion { getByText(text: string, options?: TextLocatorOptions): OperationLocator; snapshot(): ScreenSnapshot; } export interface AsyncTerminal { /** Return the session instance for a registered extension definition. */ extension(definition: TerminalExtensionDefinition): T; readonly keyboard: { press(key: KeyName | KeyPress): Promise; type(text: string, options?: TraceableInputOptions): Promise; paste(text: string, options?: TraceableInputOptions): Promise; focus(state: 'in' | 'out'): Promise; write(data: Uint8Array): Promise; }; readonly mouse: { move(point: Point, options?: MouseOptions): Promise; down(point: Point, options?: MouseOptions): Promise; up(point: Point, options?: MouseOptions): Promise; click(point: Point, options?: MouseOptions): Promise; doubleClick(point: Point, options?: MouseOptions): Promise; drag(start: Point, end: Point, options?: MouseOptions): Promise; wheel(options: WheelOptions): Promise; }; readonly process: { status(): ProcessStatus; signal(signal: string, target?: 'child' | 'process-group'): Promise; waitForExit(options?: AssertionOptions): Promise; }; readonly screen: ScreenReader; readonly revisions: AsyncRevisionObserver; readonly history: AsyncHistoryReader; readonly graphics: AsyncGraphicsReader; getByText(text: string, options?: TextLocatorOptions): AsyncLocator; region(rect: Rect): AsyncRegion; resize(viewport: Viewport): Promise; close(): Promise; } export interface OperationTerminal { readonly keyboard: { press(key: KeyName | KeyPress): Operation; type(text: string, options?: TraceableInputOptions): Operation; paste(text: string, options?: TraceableInputOptions): Operation; focus(state: 'in' | 'out'): Operation; write(data: Uint8Array): Operation; }; readonly mouse: { move(point: Point, options?: MouseOptions): Operation; down(point: Point, options?: MouseOptions): Operation; up(point: Point, options?: MouseOptions): Operation; click(point: Point, options?: MouseOptions): Operation; doubleClick(point: Point, options?: MouseOptions): Operation; drag(start: Point, end: Point, options?: MouseOptions): Operation; wheel(options: WheelOptions): Operation; }; readonly process: { status(): ProcessStatus; signal(signal: string, target?: 'child' | 'process-group'): Operation; waitForExit(options?: AssertionOptions): Operation; }; readonly screen: ScreenReader; readonly revisions: OperationRevisionObserver; readonly history: OperationHistoryReader; readonly graphics: OperationGraphicsReader; getByText(text: string, options?: TextLocatorOptions): OperationLocator; region(rect: Rect): OperationRegion; resize(viewport: Viewport): Operation; close(): Operation; } export interface OperationLocatorExpectation { toBePresent(options?: AssertionOptions): Operation; toBeAbsent(options?: StableAssertionOptions): Operation; toBeStable(options?: StableAssertionOptions): Operation; /** Every cell of the match must satisfy `style`. */ toHaveStyle(style: StyleQuery, options?: AssertionOptions): Operation; /** The terminal cursor must sit inside the match's range. */ toContainCursor(options?: AssertionOptions): Operation; } export interface AsyncLocatorExpectation { toBePresent(options?: AssertionOptions): Promise; toBeAbsent(options?: StableAssertionOptions): Promise; toBeStable(options?: StableAssertionOptions): Promise; /** Every cell of the match must satisfy `style`. */ toHaveStyle(style: StyleQuery, options?: AssertionOptions): Promise; /** The terminal cursor must sit inside the match's range. */ toContainCursor(options?: AssertionOptions): Promise; } export interface OperationTerminalExpectation { toSatisfy( predicate: (snapshot: ScreenSnapshot) => boolean, options?: StableAssertionOptions, ): Operation; toHaveShown( predicate: (snapshot: ScreenSnapshot) => boolean, options?: TransientAssertionOptions, ): Operation; toHaveShownText(text: string, options?: TransientAssertionOptions): Operation; } export interface AsyncTerminalExpectation { toSatisfy( predicate: (snapshot: ScreenSnapshot) => boolean, options?: StableAssertionOptions, ): Promise; toHaveShown( predicate: (snapshot: ScreenSnapshot) => boolean, options?: TransientAssertionOptions, ): Promise; toHaveShownText(text: string, options?: TransientAssertionOptions): Promise; }