/** * Never hand a HALF of an escape sequence to the program being relayed to. * * The terminal delivers input in whatever chunks it likes, and a single sequence * can straddle two of them. Forwarding chunk by chunk then delivers `ESC[<` in * one write and `35;112;43M` in the next, and a reader that does not carry state * across reads swallows the prefix and shows the remainder as typed text. That * is exactly what turned up in Claude's input box: * * 35;112;43M35;125;50M * * which is two mouse reports with their `ESC[<` prefixes gone. Mouse tracking * produces a flood of them, so it happened again and again. * * So a chunk that ends part-way through a sequence is held back until the rest * arrives. A lone Escape keypress looks identical to the START of a sequence, * so something has to give up waiting eventually. * * WHAT IS GIVEN UP MATTERS, and getting it wrong reintroduced the whole bug. * The first version flushed whatever it was holding, which meant an unfinished * `ESC[<35;101` was written on its own; the reader consumed the prefix, the * rest arrived in the next chunk, and `;10M` appeared in the input box. The * buffer built to prevent split sequences was splitting them itself, once per * pause in mouse movement. Measured against the shipped build: * * FLUSH "\x1b[<35;101" <- forwarded on its own * READY ";10M\x1b[<35;102;11M" <- and the tail lands as text * * So only a HELD ESCAPE THAT COULD BE A KEYPRESS is ever flushed. Anything * already known to be an unfinished sequence is waited on, and dropped if it * never completes: a truncated mouse report is worth nothing, and forwarding * it can only corrupt what the reader shows. */ /** * Split text into what is safe to forward now and a trailing part-sequence. * * Only the TAIL can be incomplete: anything before the last Escape has already * been terminated or is ordinary text. */ export declare function splitTrailingPartial(text: string): { ready: string; pending: string; }; /** * Could this held text be a real Escape KEYPRESS rather than the start of a * sequence the terminal has not finished sending? * * A bare Escape can be either, and waiting forever would swallow the key. Once * an introducer has arrived (`ESC [`, `ESC O`, `ESC ]`, ...) it is no longer * ambiguous: the terminal is mid-sequence, and what is held is a fragment that * must never reach the reader as text. * * `ESC` followed by an ordinary character is Alt+key, which is whole already * and never reaches this question. */ export declare function couldBeEscapeKey(held: string): boolean; export interface EscapeBufferOptions { /** How long to hold a lone Escape before treating it as the key. */ flushAfterMs?: number; /** * How long to wait for an unfinished SEQUENCE before giving up on it. A real * one completes within microseconds, so anything still unfinished this much * later is debris; it is dropped rather than forwarded, because forwarding a * truncated sequence is what puts stray characters on the screen. */ abandonAfterMs?: number; setTimer?: (fn: () => void, ms: number) => unknown; clearTimer?: (handle: unknown) => void; /** Injected in tests, so the suffix window does not depend on real time. */ now?: () => number; } /** * The remains of a mouse report whose beginning was dropped. * * Dropping the beginning is only half the job. The rest of that report still * arrives, and it carries no Escape, so nothing downstream can tell it from * something the operator typed: `;10M` is forwarded and shown, which is the * `;30M` in the reported screenshot. So what was abandoned is remembered just * long enough to swallow its own tail. */ interface ExpectedTail { /** * The parameters the dropped report has been given so far, fragment * included. Kept in full rather than counted, because only the actual text * can say whether what arrives could FINISH a real report: counting * separators alone accepted `35;101;` with an empty Cy, and turned a typed * `12M` into `M` by eating the digits. */ paramsSoFar: string; } /** * What, if anything, an abandoned fragment will send along afterwards. * * Only the SGR form, and deliberately. The original encoding's payload is * three RAW bytes that can be any character at all, so `abc` typed after an * abandoned `ESC [ M` is indistinguishable from a real report's payload, and * eating three real keystrokes is a worse failure than showing three stray * characters. A genuine payload follows its prefix in the same breath anyway, * so it is never the thing that is still missing when the wait runs out. */ export declare function tailExpectedAfter(fragment: string): ExpectedTail | null; /** * Eat the part of `chunk` that belongs to a report already dropped. * * Deliberately narrow, because everything it takes is something the operator * might have typed. Only digits and semicolons, optionally finished by M or m, * and a lone final byte only when the dropped report already had its * parameters. So "hello" survives, and so does a bare "M" typed after a * fragment that was nowhere near complete. */ export declare function consumeExpectedTail(chunk: string, expected: ExpectedTail): { rest: string; still: ExpectedTail | null; }; export interface EscapeBuffer { /** Feed a chunk; returns what should be forwarded now (may be empty). */ push(chunk: string): string; /** Give up anything held and return it, for shutdown. */ drain(): string; /** * Forget anything held, forwarding nothing. * * Used when the reader changes (an account swap starts a new session): a * fragment held for the old one is meaningless to the new one, and flushing * it into a fresh input box is the same stray-characters bug by another * route. */ reset(): void; /** How many fragments have been abandoned, for diagnostics. */ abandoned(): number; } /** * Reassembles input so no escape sequence is ever forwarded in pieces. * * `onFlush` receives a held part-sequence when the wait runs out, which is how a * lone Escape keypress still reaches the program. */ export declare function createEscapeBuffer(onFlush: (text: string) => void, options?: EscapeBufferOptions): EscapeBuffer; export {};