Download packages/pi-tui/src/stdin-buffer.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/pi-tui/src/stdin-buffer.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/pi-tui/src/stdin-buffer.ts
-
curl -L -o stdin-buffer.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/pi-tui/src/stdin-buffer.ts
12.1 kB
| /** | |
| * StdinBuffer buffers input and emits complete sequences. | |
| * | |
| * This is necessary because stdin data events can arrive in partial chunks, | |
| * especially for escape sequences like mouse events. Without buffering, | |
| * partial sequences can be misinterpreted as regular keypresses. | |
| * | |
| * For example, the mouse SGR sequence `\x1b[<35;20;5m` might arrive as: | |
| * - Event 1: `\x1b` | |
| * - Event 2: `[<35` | |
| * - Event 3: `;20;5m` | |
| * | |
| * The buffer accumulates these until a complete sequence is detected. | |
| * Call the `process()` method to feed input data. | |
| * | |
| * Based on code from OpenTUI (https://github.com/anomalyco/opentui) | |
| * MIT License - Copyright (c) 2025 opentui | |
| */ | |
| import { EventEmitter } from "events"; | |
| const ESC = "\x1b"; | |
| const DEFAULT_SEQUENCE_TIMEOUT_MS = 50; | |
| const DEFAULT_ESCAPE_TIMEOUT_MS = 10; | |
| const BRACKETED_PASTE_START = "\x1b[200~"; | |
| const BRACKETED_PASTE_END = "\x1b[201~"; | |
| /** | |
| * Check if a string is a complete escape sequence or needs more data | |
| */ | |
| function isCompleteSequence(data: string): "complete" | "incomplete" | "not-escape" { | |
| if (!data.startsWith(ESC)) { | |
| return "not-escape"; | |
| } | |
| if (data.length === 1) { | |
| return "incomplete"; | |
| } | |
| const afterEsc = data.slice(1); | |
| // CSI sequences: ESC [ | |
| if (afterEsc.startsWith("[")) { | |
| // Check for old-style mouse sequence: ESC[M + 3 bytes | |
| if (afterEsc.startsWith("[M")) { | |
| // Old-style mouse needs ESC[M + 3 bytes = 6 total | |
| return data.length >= 6 ? "complete" : "incomplete"; | |
| } | |
| return isCompleteCsiSequence(data); | |
| } | |
| // OSC sequences: ESC ] | |
| if (afterEsc.startsWith("]")) { | |
| return isCompleteOscSequence(data); | |
| } | |
| // DCS sequences: ESC P ... ESC \ (includes XTVersion responses) | |
| if (afterEsc.startsWith("P")) { | |
| return isCompleteDcsSequence(data); | |
| } | |
| // APC sequences: ESC _ ... ESC \ (includes Kitty graphics responses) | |
| if (afterEsc.startsWith("_")) { | |
| return isCompleteApcSequence(data); | |
| } | |
| // SS3 sequences: ESC O | |
| if (afterEsc.startsWith("O")) { | |
| // ESC O followed by a single character | |
| return afterEsc.length >= 2 ? "complete" : "incomplete"; | |
| } | |
| // Meta key sequences: ESC followed by a single character | |
| if (afterEsc.length === 1) { | |
| return "complete"; | |
| } | |
| // Unknown escape sequence - treat as complete | |
| return "complete"; | |
| } | |
| /** | |
| * Check if CSI sequence is complete | |
| * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E) | |
| */ | |
| function isCompleteCsiSequence(data: string): "complete" | "incomplete" { | |
| if (!data.startsWith(`${ESC}[`)) { | |
| return "complete"; | |
| } | |
| // Need at least ESC [ and one more character | |
| if (data.length < 3) { | |
| return "incomplete"; | |
| } | |
| const payload = data.slice(2); | |
| // CSI sequences end with a byte in the range 0x40-0x7E (@-~) | |
| // This includes all letters and several special characters | |
| const lastChar = payload[payload.length - 1]!; | |
| const lastCharCode = lastChar.charCodeAt(0); | |
| if (lastCharCode >= 0x40 && lastCharCode <= 0x7e) { | |
| // Special handling for SGR mouse sequences | |
| // Format: ESC[<B;X;Ym or ESC[<B;X;YM | |
| if (payload.startsWith("<")) { | |
| // Must have format: <digits;digits;digits[Mm] | |
| const mouseMatch = /^<\d+;\d+;\d+[Mm]$/.test(payload); | |
| if (mouseMatch) { | |
| return "complete"; | |
| } | |
| // If it ends with M or m but doesn't match the pattern, still incomplete | |
| if (lastChar === "M" || lastChar === "m") { | |
| // Check if we have the right structure | |
| const parts = payload.slice(1, -1).split(";"); | |
| if (parts.length === 3 && parts.every((p) => /^\d+$/.test(p))) { | |
| return "complete"; | |
| } | |
| } | |
| return "incomplete"; | |
| } | |
| return "complete"; | |
| } | |
| return "incomplete"; | |
| } | |
| /** | |
| * Check if OSC sequence is complete | |
| * OSC sequences: ESC ] ... ST (where ST is ESC \ or BEL) | |
| */ | |
| function isCompleteOscSequence(data: string): "complete" | "incomplete" { | |
| if (!data.startsWith(`${ESC}]`)) { | |
| return "complete"; | |
| } | |
| // OSC sequences end with ST (ESC \) or BEL (\x07) | |
| if (data.endsWith(`${ESC}\\`) || data.endsWith("\x07")) { | |
| return "complete"; | |
| } | |
| return "incomplete"; | |
| } | |
| /** | |
| * Check if DCS (Device Control String) sequence is complete | |
| * DCS sequences: ESC P ... ST (where ST is ESC \) | |
| * Used for XTVersion responses like ESC P >| ... ESC \ | |
| */ | |
| function isCompleteDcsSequence(data: string): "complete" | "incomplete" { | |
| if (!data.startsWith(`${ESC}P`)) { | |
| return "complete"; | |
| } | |
| // DCS sequences end with ST (ESC \) | |
| if (data.endsWith(`${ESC}\\`)) { | |
| return "complete"; | |
| } | |
| return "incomplete"; | |
| } | |
| /** | |
| * Check if APC (Application Program Command) sequence is complete | |
| * APC sequences: ESC _ ... ST (where ST is ESC \) | |
| * Used for Kitty graphics responses like ESC _ G ... ESC \ | |
| */ | |
| function isCompleteApcSequence(data: string): "complete" | "incomplete" { | |
| if (!data.startsWith(`${ESC}_`)) { | |
| return "complete"; | |
| } | |
| // APC sequences end with ST (ESC \) | |
| if (data.endsWith(`${ESC}\\`)) { | |
| return "complete"; | |
| } | |
| return "incomplete"; | |
| } | |
| /** | |
| * Split accumulated buffer into complete sequences | |
| */ | |
| function parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined { | |
| const match = sequence.match(/^\x1b\[(\d+)(?::\d*)?(?::\d+)?u$/); | |
| if (!match) return undefined; | |
| const codepoint = parseInt(match[1]!, 10); | |
| return codepoint >= 32 ? codepoint : undefined; | |
| } | |
| function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } { | |
| const sequences: string[] = []; | |
| let pos = 0; | |
| while (pos < buffer.length) { | |
| const remaining = buffer.slice(pos); | |
| // Try to extract a sequence starting at this position | |
| if (remaining.startsWith(ESC)) { | |
| // Find the end of this escape sequence | |
| let seqEnd = 1; | |
| while (seqEnd <= remaining.length) { | |
| const candidate = remaining.slice(0, seqEnd); | |
| const status = isCompleteSequence(candidate); | |
| if (status === "complete") { | |
| // WezTerm with enable_kitty_keyboard sends the Escape key press as a | |
| // raw '\x1b' byte (simple text path in encode_kitty, ignoring | |
| // DISAMBIGUATE_ESCAPE_CODES) and the release as a full Kitty CSI-u | |
| // sequence. These arrive concatenated as '\x1b\x1b[27;...u'. | |
| // The buffer would normally treat '\x1b\x1b' as a complete meta-key | |
| // sequence (ESC + single char), leaving '[27;...u' to be typed as | |
| // plain text. If the character immediately following '\x1b\x1b' | |
| // would begin a new escape sequence, emit only the first ESC and | |
| // restart from the second. | |
| if (candidate === "\x1b\x1b") { | |
| const nextChar = remaining[seqEnd]; | |
| if ( | |
| nextChar === "[" || // CSI | |
| nextChar === "]" || // OSC | |
| nextChar === "O" || // SS3 | |
| nextChar === "P" || // DCS | |
| nextChar === "_" // APC | |
| ) { | |
| sequences.push(ESC); | |
| pos += 1; | |
| break; | |
| } | |
| } | |
| sequences.push(candidate); | |
| pos += seqEnd; | |
| break; | |
| } else if (status === "incomplete") { | |
| seqEnd++; | |
| } else { | |
| // Should not happen when starting with ESC | |
| sequences.push(candidate); | |
| pos += seqEnd; | |
| break; | |
| } | |
| } | |
| if (seqEnd > remaining.length) { | |
| return { sequences, remainder: remaining }; | |
| } | |
| } else { | |
| // Not an escape sequence - take a single character | |
| sequences.push(remaining[0]!); | |
| pos++; | |
| } | |
| } | |
| return { sequences, remainder: "" }; | |
| } | |
| export type StdinBufferOptions = { | |
| /** | |
| * Maximum time to wait for an incomplete sequence such as CSI or mouse | |
| * (default: 50ms). | |
| */ | |
| timeout?: number; | |
| /** | |
| * Maximum time to wait after a lone ESC before treating it as Escape | |
| * (default: 10ms). Increase for high-latency Alt+key input (SSH). | |
| */ | |
| escapeTimeout?: number; | |
| }; | |
| export type StdinBufferEventMap = { | |
| data: [string]; | |
| paste: [string]; | |
| }; | |
| /** | |
| * Buffers stdin input and emits complete sequences via the 'data' event. | |
| * Handles partial escape sequences that arrive across multiple chunks. | |
| */ | |
| export class StdinBuffer extends EventEmitter<StdinBufferEventMap> { | |
| private buffer: string = ""; | |
| private timeout: ReturnType<typeof setTimeout> | null = null; | |
| private readonly timeoutMs: number; | |
| private readonly escapeTimeoutMs: number; | |
| private pasteMode: boolean = false; | |
| private pasteBuffer: string = ""; | |
| private pendingKittyPrintableCodepoint: number | undefined; | |
| constructor(options: StdinBufferOptions = {}) { | |
| super(); | |
| this.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS; | |
| this.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS; | |
| } | |
| public process(data: string | Buffer): void { | |
| // Clear any pending timeout | |
| if (this.timeout) { | |
| clearTimeout(this.timeout); | |
| this.timeout = null; | |
| } | |
| // Handle high-byte conversion (for compatibility with parseKeypress) | |
| // If buffer has single byte > 127, convert to ESC + (byte - 128) | |
| let str: string; | |
| if (Buffer.isBuffer(data)) { | |
| if (data.length === 1 && data[0]! > 127) { | |
| const byte = data[0]! - 128; | |
| str = `\x1b${String.fromCharCode(byte)}`; | |
| } else { | |
| str = data.toString(); | |
| } | |
| } else { | |
| str = data; | |
| } | |
| if (str.length === 0 && this.buffer.length === 0) { | |
| this.emitDataSequence(""); | |
| return; | |
| } | |
| this.buffer += str; | |
| if (this.pasteMode) { | |
| this.pasteBuffer += this.buffer; | |
| this.buffer = ""; | |
| const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); | |
| if (endIndex !== -1) { | |
| const pastedContent = this.pasteBuffer.slice(0, endIndex); | |
| const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length); | |
| this.pasteMode = false; | |
| this.pasteBuffer = ""; | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| this.emit("paste", pastedContent); | |
| if (remaining.length > 0) { | |
| this.process(remaining); | |
| } | |
| } | |
| return; | |
| } | |
| const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START); | |
| if (startIndex !== -1) { | |
| if (startIndex > 0) { | |
| const beforePaste = this.buffer.slice(0, startIndex); | |
| const result = extractCompleteSequences(beforePaste); | |
| for (const sequence of result.sequences) { | |
| this.emitDataSequence(sequence); | |
| } | |
| } | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length); | |
| this.pasteMode = true; | |
| this.pasteBuffer = this.buffer; | |
| this.buffer = ""; | |
| const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); | |
| if (endIndex !== -1) { | |
| const pastedContent = this.pasteBuffer.slice(0, endIndex); | |
| const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length); | |
| this.pasteMode = false; | |
| this.pasteBuffer = ""; | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| this.emit("paste", pastedContent); | |
| if (remaining.length > 0) { | |
| this.process(remaining); | |
| } | |
| } | |
| return; | |
| } | |
| const result = extractCompleteSequences(this.buffer); | |
| this.buffer = result.remainder; | |
| for (const sequence of result.sequences) { | |
| this.emitDataSequence(sequence); | |
| } | |
| if (this.buffer.length > 0) { | |
| const timeoutMs = this.buffer === ESC ? this.escapeTimeoutMs : this.timeoutMs; | |
| this.timeout = setTimeout(() => { | |
| const flushed = this.flush(); | |
| for (const sequence of flushed) { | |
| this.emitDataSequence(sequence); | |
| } | |
| }, timeoutMs); | |
| } | |
| } | |
| private emitDataSequence(sequence: string): void { | |
| const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined; | |
| if (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) { | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| return; | |
| } | |
| this.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence); | |
| this.emit("data", sequence); | |
| } | |
| flush(): string[] { | |
| if (this.timeout) { | |
| clearTimeout(this.timeout); | |
| this.timeout = null; | |
| } | |
| if (this.buffer.length === 0) { | |
| return []; | |
| } | |
| const sequences = [this.buffer]; | |
| this.buffer = ""; | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| return sequences; | |
| } | |
| clear(): void { | |
| if (this.timeout) { | |
| clearTimeout(this.timeout); | |
| this.timeout = null; | |
| } | |
| this.buffer = ""; | |
| this.pasteMode = false; | |
| this.pasteBuffer = ""; | |
| this.pendingKittyPrintableCodepoint = undefined; | |
| } | |
| getBuffer(): string { | |
| return this.buffer; | |
| } | |
| destroy(): void { | |
| this.clear(); | |
| } | |
| } | |