Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/bombshell-dev/playground.
Something went wrong. Try again.
17 kB · 530 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531import 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<TCommit = unknown> { number: number; namespace: string; /** Maximum bytes retained for an incomplete registered sequence. */ maxBufferedBytes: number; decode(message: RegisteredOscMessage): TCommit;}
export interface ExtensionRevision<T = unknown> { sequence: number; timestamp: number; extensionId: string; protocolFrame: number; screenSequence: number; value: T;}
export interface ExtensionCommit<T> { protocolFrame: number; value: T;}
export interface ExtensionSessionContext<T = unknown> { readonly terminal: AsyncTerminal; readonly screen: ScreenReader; publish(commit: ExtensionCommit<T>): ExtensionRevision<T>; diagnostic(error: GhostwrightError): void;}
export interface TerminalExtensionDefinition<TSession = unknown, TCommit = unknown> { readonly id: string; readonly osc?: OscRegistration<TCommit>; createSession(context: ExtensionSessionContext<TCommit>): TSession; accept?(session: TSession, commit: TCommit, context: ExtensionSessionContext<TCommit>): void;}
export interface TerminalLaunchOptions { command: string; args?: readonly string[]; cwd?: string; env?: Readonly<Record<string, string>>; 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<unknown, unknown>[];}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<Record<number, boolean>>;}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<Viewport>; 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<ScreenLine, 'row'> { /** 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<RevisionCollection>;}export interface OperationRevisionObserver { collect(options: RevisionCollectionOptions): Operation<RevisionCollection>;}export interface AsyncHistoryReader { read(query?: HistoryQuery): Promise<HistoryRange>; findText(text: string, options?: HistorySearchOptions): Promise<readonly HistoryMatch[]>;}export interface OperationHistoryReader { read(query?: HistoryQuery): Operation<HistoryRange>; findText(text: string, options?: HistorySearchOptions): Operation<readonly HistoryMatch[]>;}export interface AsyncGraphicsReader { inspectImage(id: number): Promise<KittyImageSnapshot | undefined>; copyImageData(id: number): Promise<Uint8Array | undefined>;}export interface OperationGraphicsReader { inspectImage(id: number): Operation<KittyImageSnapshot | undefined>; copyImageData(id: number): Operation<Uint8Array | undefined>;}
export interface AsyncLocator { nth(index: number): AsyncLocator; region(rect: Rect): AsyncLocator; matches(): readonly LocatorMatch[]; click(options?: MouseOptions): Promise<ActionReceipt>;}export interface OperationLocator { nth(index: number): OperationLocator; region(rect: Rect): OperationLocator; matches(): readonly LocatorMatch[]; click(options?: MouseOptions): Operation<ActionReceipt>;}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<T>(definition: TerminalExtensionDefinition<T, unknown>): T; readonly keyboard: { press(key: KeyName | KeyPress): Promise<ActionReceipt>; type(text: string, options?: TraceableInputOptions): Promise<ActionReceipt>; paste(text: string, options?: TraceableInputOptions): Promise<ActionReceipt>; focus(state: 'in' | 'out'): Promise<ActionReceipt>; write(data: Uint8Array): Promise<ActionReceipt>; }; readonly mouse: { move(point: Point, options?: MouseOptions): Promise<ActionReceipt>; down(point: Point, options?: MouseOptions): Promise<ActionReceipt>; up(point: Point, options?: MouseOptions): Promise<ActionReceipt>; click(point: Point, options?: MouseOptions): Promise<ActionReceipt>; doubleClick(point: Point, options?: MouseOptions): Promise<ActionReceipt>; drag(start: Point, end: Point, options?: MouseOptions): Promise<ActionReceipt>; wheel(options: WheelOptions): Promise<ActionReceipt>; }; readonly process: { status(): ProcessStatus; signal(signal: string, target?: 'child' | 'process-group'): Promise<ActionReceipt>; waitForExit(options?: AssertionOptions): Promise<ProcessStatus>; }; 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<ActionReceipt>; close(): Promise<ActionReceipt>;}export interface OperationTerminal { readonly keyboard: { press(key: KeyName | KeyPress): Operation<ActionReceipt>; type(text: string, options?: TraceableInputOptions): Operation<ActionReceipt>; paste(text: string, options?: TraceableInputOptions): Operation<ActionReceipt>; focus(state: 'in' | 'out'): Operation<ActionReceipt>; write(data: Uint8Array): Operation<ActionReceipt>; }; readonly mouse: { move(point: Point, options?: MouseOptions): Operation<ActionReceipt>; down(point: Point, options?: MouseOptions): Operation<ActionReceipt>; up(point: Point, options?: MouseOptions): Operation<ActionReceipt>; click(point: Point, options?: MouseOptions): Operation<ActionReceipt>; doubleClick(point: Point, options?: MouseOptions): Operation<ActionReceipt>; drag(start: Point, end: Point, options?: MouseOptions): Operation<ActionReceipt>; wheel(options: WheelOptions): Operation<ActionReceipt>; }; readonly process: { status(): ProcessStatus; signal(signal: string, target?: 'child' | 'process-group'): Operation<ActionReceipt>; waitForExit(options?: AssertionOptions): Operation<ProcessStatus>; }; 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<ActionReceipt>; close(): Operation<ActionReceipt>;}
export interface OperationLocatorExpectation { toBePresent(options?: AssertionOptions): Operation<LocatorMatch>; toBeAbsent(options?: StableAssertionOptions): Operation<void>; toBeStable(options?: StableAssertionOptions): Operation<LocatorMatch>; /** Every cell of the match must satisfy `style`. */ toHaveStyle(style: StyleQuery, options?: AssertionOptions): Operation<LocatorMatch>; /** The terminal cursor must sit inside the match's range. */ toContainCursor(options?: AssertionOptions): Operation<LocatorMatch>;}export interface AsyncLocatorExpectation { toBePresent(options?: AssertionOptions): Promise<LocatorMatch>; toBeAbsent(options?: StableAssertionOptions): Promise<void>; toBeStable(options?: StableAssertionOptions): Promise<LocatorMatch>; /** Every cell of the match must satisfy `style`. */ toHaveStyle(style: StyleQuery, options?: AssertionOptions): Promise<LocatorMatch>; /** The terminal cursor must sit inside the match's range. */ toContainCursor(options?: AssertionOptions): Promise<LocatorMatch>;}export interface OperationTerminalExpectation { toSatisfy( predicate: (snapshot: ScreenSnapshot) => boolean, options?: StableAssertionOptions, ): Operation<ScreenSnapshot>; toHaveShown( predicate: (snapshot: ScreenSnapshot) => boolean, options?: TransientAssertionOptions, ): Operation<ScreenRevision>; toHaveShownText(text: string, options?: TransientAssertionOptions): Operation<ScreenRevision>;}export interface AsyncTerminalExpectation { toSatisfy( predicate: (snapshot: ScreenSnapshot) => boolean, options?: StableAssertionOptions, ): Promise<ScreenSnapshot>; toHaveShown( predicate: (snapshot: ScreenSnapshot) => boolean, options?: TransientAssertionOptions, ): Promise<ScreenRevision>; toHaveShownText(text: string, options?: TransientAssertionOptions): Promise<ScreenRevision>;}