/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */ /** * @param {unknown} chunk * @returns {number[]} */ function bareEditChunkBytes(chunk) { if (chunk == null) return [] if (typeof chunk === 'string') { const out = [] for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff) return out } const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length const out = [] for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff) return out } /** * @param {{ nextByte: () => Promise, dispose?: () => void, _keyq?: number[] }} reader */ async function bareEditReadKey(reader) { reader._keyq = reader._keyq || [] const q = reader._keyq for (;;) { const pasteText = bareEditTryConsumeBracketedPaste(q) if (pasteText !== undefined && pasteText !== null) { return { type: 'paste', text: pasteText } } if (pasteText === null) { const b = await reader.nextByte() if (b === undefined) { const eofPaste = bareEditFinalizeBracketedPasteOnEof(q) if (eofPaste !== undefined) { return { type: 'paste', text: eofPaste } } if (!q.length) return { type: 'eof' } if (q.length === 1 && q[0] === 27) { q.length = 0 return { type: 'key', ch: '\x1b' } } const lone = q.shift() if (lone !== undefined && lone < 0x20) { return { type: 'ctrl', code: lone } } if (lone !== undefined) { return { type: 'key', ch: String.fromCharCode(lone) } } return { type: 'eof' } } q.push(b) continue } const ev = bareEditTryConsumeKey(q) if (ev) return ev const b = await reader.nextByte() if (b === undefined) { const eofPaste = bareEditFinalizeBracketedPasteOnEof(q) if (eofPaste !== undefined) { return { type: 'paste', text: eofPaste } } if (!q.length) return { type: 'eof' } if (q.length === 1 && q[0] === 27) { q.length = 0 return { type: 'key', ch: '\x1b' } } const lone = q.shift() if (lone !== undefined && lone < 0x20) { return { type: 'ctrl', code: lone } } if (lone !== undefined) { return { type: 'key', ch: String.fromCharCode(lone) } } return { type: 'eof' } } q.push(b) } } /** * @param {import('stream').Readable} stdin */ function bareEditCreateStdinReader(stdin) { /** @type {number[]} */ const bytes = [] /** @type {(() => void)[]} */ const waiters = [] function drain() { while (waiters.length && bytes.length) { const w = waiters.shift() if (w) w() } } /** @param {unknown} chunk */ function onData(chunk) { bytes.push(...bareEditChunkBytes(chunk)) drain() } stdin.on('data', onData) return { nextByte() { if (bytes.length) return Promise.resolve(bytes.shift()) return new Promise((resolve) => { waiters.push(() => resolve(bytes.shift())) }) }, dispose() { stdin.removeListener('data', onData) } } }