Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.8 kB · 212 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213/** * Query the terminal and await responses without timeouts. * * Terminal queries (DECRQM, DA1, OSC 11, etc.) share the stdin stream * with keyboard input. Response sequences are syntactically * distinguishable from key events, so the input parser recognizes them * and dispatches them here. * * To avoid timeouts, each query batch is terminated by a DA1 sentinel * (CSI c) — every terminal since VT100 responds to DA1, and terminals * answer queries in order. So: if your query's response arrives before * DA1's, the terminal supports it; if DA1 arrives first, it doesn't. * * Usage: * const [sync, grapheme] = await Promise.all([ * querier.send(decrqm(2026)), * querier.send(decrqm(2027)), * querier.flush(), * ]) * // sync and grapheme are DECRPM responses or undefined if unsupported */
import type { TerminalResponse } from './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. Built by `decrqm()`, * `oscColor()`, `kittyKeyboard()`, etc. */export type TerminalQuery<T extends TerminalResponse = TerminalResponse> = { /** 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<TerminalResponse, { type: 'decrpm' }>type Da1Response = Extract<TerminalResponse, { type: 'da1' }>type Da2Response = Extract<TerminalResponse, { type: 'da2' }>type KittyResponse = Extract<TerminalResponse, { type: 'kittyKeyboard' }>type CursorPosResponse = Extract<TerminalResponse, { type: 'cursorPosition' }>type OscResponse = Extract<TerminalResponse, { type: 'osc' }>type XtversionResponse = Extract<TerminalResponse, { type: 'xtversion' }>
// -- Query builders --
/** DECRQM: request DEC private mode status (CSI ? mode $ p). * Terminal replies with DECRPM (CSI ? mode ; status $ y) or ignores. */export function decrqm(mode: number): TerminalQuery<DecrpmResponse> { return { request: csi(`?${mode}$p`), match: (r): r is DecrpmResponse => r.type === 'decrpm' && r.mode === mode, }}
/** Primary Device Attributes query (CSI c). Every terminal answers this — * used internally by flush() as a universal sentinel. Call directly if * you want the DA1 params. */export function da1(): TerminalQuery<Da1Response> { return { request: csi('c'), match: (r): r is Da1Response => r.type === 'da1', }}
/** Secondary Device Attributes query (CSI > c). Returns terminal version. */export function da2(): TerminalQuery<Da2Response> { return { request: csi('>c'), match: (r): r is Da2Response => r.type === 'da2', }}
/** Query current Kitty keyboard protocol flags (CSI ? u). * Terminal replies with CSI ? flags u or ignores. */export function kittyKeyboard(): TerminalQuery<KittyResponse> { return { request: csi('?u'), match: (r): r is KittyResponse => r.type === 'kittyKeyboard', }}
/** DECXCPR: request cursor position with DEC-private marker (CSI ? 6 n). * Terminal replies with CSI ? row ; col R. The `?` marker is critical — * the plain DSR form (CSI 6 n → CSI row;col R) is ambiguous with * modified F3 keys (Shift+F3 = CSI 1;2 R, etc.). */export function cursorPosition(): TerminalQuery<CursorPosResponse> { return { request: csi('?6n'), match: (r): r is CursorPosResponse => r.type === 'cursorPosition', }}
/** OSC dynamic color query (e.g. OSC 11 for bg color, OSC 10 for fg). * The `?` data slot asks the terminal to reply with the current value. */export function oscColor(code: number): TerminalQuery<OscResponse> { return { request: osc(code, '?'), match: (r): r is OscResponse => r.type === 'osc' && r.code === code, }}
/** XTVERSION: request terminal name/version (CSI > 0 q). * Terminal replies with DCS > | name ST (e.g. "xterm.js(5.5.0)") or ignores. * This survives SSH — the query goes through the pty, not the environment, * so it identifies the *client* terminal even when TERM_PROGRAM isn't * forwarded. Used to detect xterm.js for wheel-scroll compensation. */export function xtversion(): TerminalQuery<XtversionResponse> { return { request: csi('>0q'), match: (r): r is XtversionResponse => r.type === 'xtversion', }}
// -- Querier --
/** Sentinel request sequence (DA1). Kept internal; flush() writes it. */const SENTINEL = csi('c')
type Pending = | { kind: 'query' match: (r: TerminalResponse) => boolean resolve: (r: TerminalResponse | undefined) => void } | { kind: 'sentinel'; resolve: () => void }
export class TerminalQuerier { /** * Interleaved queue of queries and sentinels in send order. Terminals * respond in order, so each flush() barrier only drains queries queued * before it — concurrent batches from independent callers stay isolated. */ private queue: Pending[] = []
constructor(private stdout: NodeJS.WriteStream) {}
/** * Send a query and wait for its response. * * Resolves with the response when `query.match` matches an incoming * TerminalResponse, or with `undefined` when a flush() sentinel arrives * before any matching response (meaning the terminal ignored the query). * * Never rejects; never times out on its own. If you never call flush() * and the terminal doesn't respond, the promise remains pending. */ send<T extends TerminalResponse>( query: TerminalQuery<T>, ): Promise<T | undefined> { return new Promise(resolve => { this.queue.push({ kind: 'query', match: query.match, resolve: r => resolve(r as T | undefined), }) this.stdout.write(query.request) }) }
/** * Send the DA1 sentinel. Resolves when DA1's response arrives. * * As a side effect, all queries still pending when DA1 arrives are * resolved with `undefined` (terminal didn't respond → doesn't support * the query). This is the barrier that makes send() timeout-free. * * Safe to call with no pending queries — still waits for a round-trip. */ flush(): Promise<void> { return new Promise(resolve => { this.queue.push({ kind: 'sentinel', resolve }) this.stdout.write(SENTINEL) }) }
/** * Dispatch a response parsed from stdin. Called by App.tsx's * processKeysInBatch for every `kind: 'response'` item. * * Matching strategy: * - First, try to match a pending query (FIFO, first match wins). * This lets callers send(da1()) explicitly if they want the DA1 * params — a separate DA1 write means the terminal sends TWO DA1 * responses. The first matches the explicit query; the second * (unmatched) fires the sentinel. * - Otherwise, if this is a DA1, fire the FIRST pending sentinel: * resolve any queries queued before that sentinel with undefined * (the terminal answered DA1 without answering them → unsupported) * and signal its flush() completion. Only draining up to the first * sentinel keeps later batches intact when multiple callers have * concurrent queries in flight. * - Unsolicited responses (no match, no sentinel) are silently dropped. */ 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() } } }}