/** * Query the terminal and await responses without timeouts. */ import type { TerminalResponse } from '../core/parse-keypress.js' import { csi } from '../termio/csi.js' import { osc } from '../termio/osc.js' /** A terminal query: an outbound request sequence paired with a matcher * that recognizes the expected inbound response. */ export type TerminalQuery = { /** Escape sequence to write to stdout */ request: string /** Recognizes the expected response in the inbound stream */ match: (r: TerminalResponse) => r is T } type DecrpmResponse = Extract type Da1Response = Extract type Da2Response = Extract type KittyResponse = Extract type CursorPosResponse = Extract type OscResponse = Extract type XtversionResponse = Extract // -- Query builders -- export function decrqm(mode: number): TerminalQuery { return { request: csi(`?${mode}$p`), match: (r): r is DecrpmResponse => r.type === 'decrpm' && r.mode === mode, } } export function da1(): TerminalQuery { return { request: csi('c'), match: (r): r is Da1Response => r.type === 'da1', } } export function da2(): TerminalQuery { return { request: csi('>c'), match: (r): r is Da2Response => r.type === 'da2', } } export function kittyKeyboard(): TerminalQuery { return { request: csi('?u'), match: (r): r is KittyResponse => r.type === 'kittyKeyboard', } } export function cursorPosition(): TerminalQuery { return { request: csi('?6n'), match: (r): r is CursorPosResponse => r.type === 'cursorPosition', } } export function oscColor(code: number): TerminalQuery { return { request: osc(code, '?'), match: (r): r is OscResponse => r.type === 'osc' && r.code === code, } } export function xtversion(): TerminalQuery { return { request: csi('>0q'), match: (r): r is XtversionResponse => r.type === 'xtversion', } } // -- Querier -- const SENTINEL = csi('c') type Pending = | { kind: 'query' match: (r: TerminalResponse) => boolean resolve: (r: TerminalResponse | undefined) => void } | { kind: 'sentinel'; resolve: () => void } export class TerminalQuerier { private queue: Pending[] = [] constructor(private stdout: NodeJS.WriteStream) {} send(query: TerminalQuery): Promise { return new Promise((resolve) => { this.queue.push({ kind: 'query', match: query.match, resolve: (r) => resolve(r as T | undefined), }) this.stdout.write(query.request) }) } flush(): Promise { return new Promise((resolve) => { this.queue.push({ kind: 'sentinel', resolve }) this.stdout.write(SENTINEL) }) } onResponse(r: TerminalResponse): void { const idx = this.queue.findIndex((p) => p.kind === 'query' && p.match(r)) if (idx !== -1) { const [q] = this.queue.splice(idx, 1) if (q?.kind === 'query') q.resolve(r) return } if (r.type === 'da1') { const s = this.queue.findIndex((p) => p.kind === 'sentinel') if (s === -1) return for (const p of this.queue.splice(0, s + 1)) { if (p.kind === 'query') p.resolve(undefined) else p.resolve() } } } }