diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 7994816efc..6390fb1ee6 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -76,6 +76,283 @@ const ALT_KEY_CHARACTER_MAP: Record = { '\u03A9': 'z', }; +/** + * Check if a buffer could potentially be a valid kitty sequence or its prefix. + */ +function couldBeKittySequence(buffer: string): boolean { + // Kitty sequences always start with ESC[. + if (buffer.length === 0) return true; + if (buffer === ESC || buffer === `${ESC}[`) return true; + + if (!buffer.startsWith(`${ESC}[`)) return false; + + // Check for known kitty sequence patterns: + // 1. ESC[ - could be CSI-u or tilde-coded + // 2. ESC[1; - parameterized functional + // 3. ESC[ - legacy functional keys + // 4. ESC[Z - reverse tab + const afterCSI = buffer.slice(2); + + // Check if it starts with a digit (could be CSI-u or parameterized) + if (/^\d/.test(afterCSI)) return true; + + // Check for known single-letter sequences + if (/^[ABCDHFPQRSZ]/.test(afterCSI)) return true; + + // Check for 1; pattern (parameterized sequences) + if (/^1;\d/.test(afterCSI)) return true; + + // Anything else starting with ESC[ that doesn't match our patterns + // is likely not a kitty sequence we handle + return false; +} + +/** + * Parses a single complete kitty/parameterized/legacy sequence from the start + * of the buffer. + * + * This enables peel-and-continue parsing for batched input, allowing us to + * "peel off" one complete event when multiple sequences arrive in a single + * chunk, preventing buffer overflow and fragmentation. + * + * @param buffer - The input buffer string to parse. + * @returns The parsed Key and the number of characters consumed, or null if + * no complete sequence is found at the start of the buffer. + */ +function parseKittyPrefix(buffer: string): { key: Key; length: number } | null { + // In older terminals ESC [ Z was used as Cursor Backward Tabulation (CBT) + // In newer terminals the same functionality of key combination for moving + // backward through focusable elements is Shift+Tab, hence we will + // map ESC [ Z to Shift+Tab + // 0) Reverse Tab (legacy): ESC [ Z + // Treat as Shift+Tab for UI purposes. + // Regex parts: + // ^ - start of buffer + // ESC [ - CSI introducer + // Z - legacy reverse tab + const revTabLegacy = new RegExp(`^${ESC}\\[Z`); + let m = buffer.match(revTabLegacy); + if (m) { + return { + key: { + name: 'tab', + ctrl: false, + meta: false, + shift: true, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + + // 1) Reverse Tab (parameterized): ESC [ 1 ; Z + // Parameterized reverse Tab: ESC [ 1 ; Z + const revTabParam = new RegExp(`^${ESC}\\[1;(\\d+)Z`); + m = buffer.match(revTabParam); + if (m) { + let mods = parseInt(m[1], 10); + if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { + mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; + } + const bits = mods - KITTY_MODIFIER_BASE; + const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; + const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; + return { + key: { + name: 'tab', + ctrl, + meta: alt, + // Reverse tab implies Shift behavior; force shift regardless of mods + shift: true, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + + // 2) Parameterized functional: ESC [ 1 ; (A|B|C|D|H|F|P|Q|R|S) + // 2) Parameterized functional: ESC [ 1 ; (A|B|C|D|H|F|P|Q|R|S) + // Arrows, Home/End, F1–F4 with modifiers encoded in . + const arrowPrefix = new RegExp(`^${ESC}\\[1;(\\d+)([ABCDHFPQSR])`); + m = buffer.match(arrowPrefix); + if (m) { + let mods = parseInt(m[1], 10); + if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { + mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; + } + const bits = mods - KITTY_MODIFIER_BASE; + const shift = (bits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT; + const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; + const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; + const sym = m[2]; + const symbolToName: { [k: string]: string } = { + A: 'up', + B: 'down', + C: 'right', + D: 'left', + H: 'home', + F: 'end', + P: 'f1', + Q: 'f2', + R: 'f3', + S: 'f4', + }; + const name = symbolToName[sym] || ''; + if (!name) return null; + return { + key: { + name, + ctrl, + meta: alt, + shift, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + + // 3) CSI-u form: ESC [ ; (u|~) + // 3) CSI-u and tilde-coded functional keys: ESC [ ; (u|~) + // 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys. + const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`); + m = buffer.match(csiUPrefix); + if (m) { + const keyCode = parseInt(m[1], 10); + let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE; + if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { + modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; + } + const modifierBits = modifiers - KITTY_MODIFIER_BASE; + const shift = (modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT; + const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; + const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; + const terminator = m[4]; + + // Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End) + if (terminator === '~') { + let name: string | null = null; + switch (keyCode) { + case 1: + name = 'home'; + break; + case 2: + name = 'insert'; + break; + case 3: + name = 'delete'; + break; + case 4: + name = 'end'; + break; + case 5: + name = 'pageup'; + break; + case 6: + name = 'pagedown'; + break; + default: + break; + } + if (name) { + return { + key: { + name, + ctrl, + meta: alt, + shift, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + } + + const kittyKeyCodeToName: { [key: number]: string } = { + [CHAR_CODE_ESC]: 'escape', + [KITTY_KEYCODE_TAB]: 'tab', + [KITTY_KEYCODE_BACKSPACE]: 'backspace', + [KITTY_KEYCODE_ENTER]: 'return', + [KITTY_KEYCODE_NUMPAD_ENTER]: 'return', + }; + + const name = kittyKeyCodeToName[keyCode]; + if (name) { + return { + key: { + name, + ctrl, + meta: alt, + shift, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + + // Ctrl+letters and Alt+letters + if ( + (ctrl || alt) && + keyCode >= 'a'.charCodeAt(0) && + keyCode <= 'z'.charCodeAt(0) + ) { + const letter = String.fromCharCode(keyCode); + return { + key: { + name: letter, + ctrl, + meta: alt, + shift, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + } + + // 4) Legacy function keys (no parameters): ESC [ (A|B|C|D|H|F) + // Arrows + Home/End without modifiers. + const legacyFuncKey = new RegExp(`^${ESC}\\[([ABCDHF])`); + m = buffer.match(legacyFuncKey); + if (m) { + const sym = m[1]; + const nameMap: { [key: string]: string } = { + A: 'up', + B: 'down', + C: 'right', + D: 'left', + H: 'home', + F: 'end', + }; + const name = nameMap[sym]!; + return { + key: { + name, + ctrl: false, + meta: false, + shift: false, + paste: false, + sequence: buffer.slice(0, m[0].length), + kittyProtocol: true, + }, + length: m[0].length, + }; + } + + return null; +} + export interface Key { name: string; ctrl: boolean; @@ -107,6 +384,21 @@ export function useKeypressContext() { return context; } +/** + * Determines if the passthrough stream workaround should be used. + * This is necessary for Node.js versions older than 20 or when the + * PASTE_WORKAROUND environment variable is set, to correctly handle + * paste events. + */ +function shouldUsePassthrough(): boolean { + const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10); + return ( + nodeMajorVersion < 20 || + process.env['PASTE_WORKAROUND'] === '1' || + process.env['PASTE_WORKAROUND'] === 'true' + ); +} + export function KeypressProvider({ children, kittyProtocolEnabled, @@ -119,332 +411,47 @@ export function KeypressProvider({ debugKeystrokeLogging?: boolean; }) { const { stdin, setRawMode } = useStdin(); - const subscribers = useRef>(new Set()).current; - const isDraggingRef = useRef(false); - const dragBufferRef = useRef(''); - const draggingTimerRef = useRef(null); + const subscribers = useRef>(new Set()).current; const subscribe = useCallback( - (handler: KeypressHandler) => { - subscribers.add(handler); - }, + (handler: KeypressHandler) => subscribers.add(handler), [subscribers], ); - const unsubscribe = useCallback( - (handler: KeypressHandler) => { - subscribers.delete(handler); - }, + (handler: KeypressHandler) => subscribers.delete(handler), + [subscribers], + ); + const broadcast = useCallback( + (key: Key) => subscribers.forEach((handler) => handler(key)), [subscribers], ); useEffect(() => { - const clearDraggingTimer = () => { - if (draggingTimerRef.current) { - clearTimeout(draggingTimerRef.current); - draggingTimerRef.current = null; - } - }; - const wasRaw = stdin.isRaw; if (wasRaw === false) { setRawMode(true); } - const keypressStream = new PassThrough(); - let usePassthrough = false; - const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10); - if ( - nodeMajorVersion < 20 || - process.env['PASTE_WORKAROUND'] === '1' || - process.env['PASTE_WORKAROUND'] === 'true' - ) { - usePassthrough = true; - } + const keypressStream = shouldUsePassthrough() ? new PassThrough() : null; - let isPaste = false; - let pasteBuffer = Buffer.alloc(0); + // If non-null that means we are in paste mode + let pasteBuffer: Buffer | null = null; + + // Used to turn "\" quickly followed by a "enter" into a shift enter + let backslashTimeout: NodeJS.Timeout | null = null; + + // Buffers incomplete Kitty sequences and timer to flush it let kittySequenceBuffer = ''; let kittySequenceTimeout: NodeJS.Timeout | null = null; - let backslashTimeout: NodeJS.Timeout | null = null; - let waitingForEnterAfterBackslash = false; - // Check if a buffer could potentially be a valid kitty sequence or its prefix - const couldBeKittySequence = (buffer: string): boolean => { - // Kitty sequences always start with ESC[. - if (buffer.length === 0) return true; - if (buffer === ESC || buffer === `${ESC}[`) return true; + // Used to detect filename drag-and-drops. + let dragBuffer = ''; + let draggingTimer: NodeJS.Timeout | null = null; - if (!buffer.startsWith(`${ESC}[`)) return false; - - // Check for known kitty sequence patterns: - // 1. ESC[ - could be CSI-u or tilde-coded - // 2. ESC[1; - parameterized functional - // 3. ESC[ - legacy functional keys - // 4. ESC[Z - reverse tab - const afterCSI = buffer.slice(2); - - // Check if it starts with a digit (could be CSI-u or parameterized) - if (/^\d/.test(afterCSI)) return true; - - // Check for known single-letter sequences - if (/^[ABCDHFPQRSZ]/.test(afterCSI)) return true; - - // Check for 1; pattern (parameterized sequences) - if (/^1;\d/.test(afterCSI)) return true; - - // Anything else starting with ESC[ that doesn't match our patterns - // is likely not a kitty sequence we handle - return false; - }; - - // Parse a single complete kitty sequence from the start (prefix) of the - // buffer and return both the Key and the number of characters consumed. - // This lets us "peel off" one complete event when multiple sequences arrive - // in a single chunk, preventing buffer overflow and fragmentation. - // Parse a single complete kitty/parameterized/legacy sequence from the start - // of the buffer and return both the parsed Key and the number of characters - // consumed. This enables peel-and-continue parsing for batched input. - const parseKittyPrefix = ( - buffer: string, - ): { key: Key; length: number } | null => { - // In older terminals ESC [ Z was used as Cursor Backward Tabulation (CBT) - // In newer terminals the same functionality of key combination for moving - // backward through focusable elements is Shift+Tab, hence we will - // map ESC [ Z to Shift+Tab - // 0) Reverse Tab (legacy): ESC [ Z - // Treat as Shift+Tab for UI purposes. - // Regex parts: - // ^ - start of buffer - // ESC [ - CSI introducer - // Z - legacy reverse tab - const revTabLegacy = new RegExp(`^${ESC}\\[Z`); - let m = buffer.match(revTabLegacy); - if (m) { - return { - key: { - name: 'tab', - ctrl: false, - meta: false, - shift: true, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - - // 1) Reverse Tab (parameterized): ESC [ 1 ; Z - // Parameterized reverse Tab: ESC [ 1 ; Z - const revTabParam = new RegExp(`^${ESC}\\[1;(\\d+)Z`); - m = buffer.match(revTabParam); - if (m) { - let mods = parseInt(m[1], 10); - if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { - mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; - } - const bits = mods - KITTY_MODIFIER_BASE; - const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; - const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; - return { - key: { - name: 'tab', - ctrl, - meta: alt, - // Reverse tab implies Shift behavior; force shift regardless of mods - shift: true, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - - // 2) Parameterized functional: ESC [ 1 ; (A|B|C|D|H|F|P|Q|R|S) - // 2) Parameterized functional: ESC [ 1 ; (A|B|C|D|H|F|P|Q|R|S) - // Arrows, Home/End, F1–F4 with modifiers encoded in . - const arrowPrefix = new RegExp(`^${ESC}\\[1;(\\d+)([ABCDHFPQSR])`); - m = buffer.match(arrowPrefix); - if (m) { - let mods = parseInt(m[1], 10); - if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { - mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; - } - const bits = mods - KITTY_MODIFIER_BASE; - const shift = (bits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT; - const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; - const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; - const sym = m[2]; - const symbolToName: { [k: string]: string } = { - A: 'up', - B: 'down', - C: 'right', - D: 'left', - H: 'home', - F: 'end', - P: 'f1', - Q: 'f2', - R: 'f3', - S: 'f4', - }; - const name = symbolToName[sym] || ''; - if (!name) return null; - return { - key: { - name, - ctrl, - meta: alt, - shift, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - - // 3) CSI-u form: ESC [ ; (u|~) - // 3) CSI-u and tilde-coded functional keys: ESC [ ; (u|~) - // 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys. - const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`); - m = buffer.match(csiUPrefix); - if (m) { - const keyCode = parseInt(m[1], 10); - let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE; - if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) { - modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET; - } - const modifierBits = modifiers - KITTY_MODIFIER_BASE; - const shift = - (modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT; - const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; - const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; - const terminator = m[4]; - - // Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End) - if (terminator === '~') { - let name: string | null = null; - switch (keyCode) { - case 1: - name = 'home'; - break; - case 2: - name = 'insert'; - break; - case 3: - name = 'delete'; - break; - case 4: - name = 'end'; - break; - case 5: - name = 'pageup'; - break; - case 6: - name = 'pagedown'; - break; - default: - break; - } - if (name) { - return { - key: { - name, - ctrl, - meta: alt, - shift, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - } - - const kittyKeyCodeToName: { [key: number]: string } = { - [CHAR_CODE_ESC]: 'escape', - [KITTY_KEYCODE_TAB]: 'tab', - [KITTY_KEYCODE_BACKSPACE]: 'backspace', - [KITTY_KEYCODE_ENTER]: 'return', - [KITTY_KEYCODE_NUMPAD_ENTER]: 'return', - }; - - const name = kittyKeyCodeToName[keyCode]; - if (name) { - return { - key: { - name, - ctrl, - meta: alt, - shift, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - - // Ctrl+letters and Alt+letters - if ( - (ctrl || alt) && - keyCode >= 'a'.charCodeAt(0) && - keyCode <= 'z'.charCodeAt(0) - ) { - const letter = String.fromCharCode(keyCode); - return { - key: { - name: letter, - ctrl, - meta: alt, - shift, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - } - - // 4) Legacy function keys (no parameters): ESC [ (A|B|C|D|H|F) - // Arrows + Home/End without modifiers. - const legacyFuncKey = new RegExp(`^${ESC}\\[([ABCDHF])`); - m = buffer.match(legacyFuncKey); - if (m) { - const sym = m[1]; - const nameMap: { [key: string]: string } = { - A: 'up', - B: 'down', - C: 'right', - D: 'left', - H: 'home', - F: 'end', - }; - const name = nameMap[sym]!; - return { - key: { - name, - ctrl: false, - meta: false, - shift: false, - paste: false, - sequence: buffer.slice(0, m[0].length), - kittyProtocol: true, - }, - length: m[0].length, - }; - } - - return null; - }; - - const broadcast = (key: Key) => { - for (const handler of subscribers) { - handler(key); + const clearDraggingTimer = () => { + if (draggingTimer) { + clearTimeout(draggingTimer); + draggingTimer = null; } }; @@ -479,24 +486,25 @@ export function KeypressProvider({ } if (key.name === 'paste-start') { flushKittyBufferOnInterrupt('paste start'); - isPaste = true; - return; - } - if (key.name === 'paste-end') { - isPaste = false; - broadcast({ - name: '', - ctrl: false, - meta: false, - shift: false, - paste: true, - sequence: pasteBuffer.toString(), - }); pasteBuffer = Buffer.alloc(0); return; } + if (key.name === 'paste-end') { + if (pasteBuffer !== null) { + broadcast({ + name: '', + ctrl: false, + meta: false, + shift: false, + paste: true, + sequence: pasteBuffer.toString(), + }); + } + pasteBuffer = null; + return; + } - if (isPaste) { + if (pasteBuffer !== null) { pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]); return; } @@ -504,16 +512,15 @@ export function KeypressProvider({ if ( key.sequence === SINGLE_QUOTE || key.sequence === DOUBLE_QUOTE || - isDraggingRef.current + draggingTimer !== null ) { - isDraggingRef.current = true; - dragBufferRef.current += key.sequence; + dragBuffer += key.sequence; clearDraggingTimer(); - draggingTimerRef.current = setTimeout(() => { - isDraggingRef.current = false; - const seq = dragBufferRef.current; - dragBufferRef.current = ''; + draggingTimer = setTimeout(() => { + draggingTimer = null; + const seq = dragBuffer; + dragBuffer = ''; if (seq) { broadcast({ ...key, name: '', paste: true, sequence: seq }); } @@ -529,18 +536,15 @@ export function KeypressProvider({ ctrl: false, meta: true, shift: false, - paste: isPaste, + paste: pasteBuffer !== null, sequence: key.sequence, }); return; } - if (key.name === 'return' && waitingForEnterAfterBackslash) { - if (backslashTimeout) { - clearTimeout(backslashTimeout); - backslashTimeout = null; - } - waitingForEnterAfterBackslash = false; + if (key.name === 'return' && backslashTimeout !== null) { + clearTimeout(backslashTimeout); + backslashTimeout = null; broadcast({ ...key, shift: true, @@ -551,21 +555,16 @@ export function KeypressProvider({ if (key.sequence === '\\' && !key.name) { // Corrected escaping for backslash - waitingForEnterAfterBackslash = true; backslashTimeout = setTimeout(() => { - waitingForEnterAfterBackslash = false; backslashTimeout = null; broadcast(key); }, BACKSLASH_ENTER_DETECTION_WINDOW_MS); return; } - if (waitingForEnterAfterBackslash && key.name !== 'return') { - if (backslashTimeout) { - clearTimeout(backslashTimeout); - backslashTimeout = null; - } - waitingForEnterAfterBackslash = false; + if (backslashTimeout !== null && key.name !== 'return') { + clearTimeout(backslashTimeout); + backslashTimeout = null; broadcast({ name: '', sequence: '\\', @@ -764,7 +763,7 @@ export function KeypressProvider({ if (key.name === 'return' && key.sequence === `${ESC}\r`) { key.meta = true; } - broadcast({ ...key, paste: isPaste }); + broadcast({ ...key, paste: pasteBuffer !== null }); }; const handleRawKeypress = (data: Buffer) => { @@ -791,13 +790,13 @@ export function KeypressProvider({ markerLength = pasteModeSuffixBuffer.length; if (nextMarkerPos === -1) { - keypressStream.write(data.slice(pos)); + keypressStream!.write(data.slice(pos)); return; } const nextData = data.slice(pos, nextMarkerPos); if (nextData.length > 0) { - keypressStream.write(nextData); + keypressStream!.write(nextData); } const createPasteKeyEvent = ( name: 'paste-start' | 'paste-end', @@ -819,7 +818,7 @@ export function KeypressProvider({ }; let rl: readline.Interface; - if (usePassthrough) { + if (keypressStream !== null) { rl = readline.createInterface({ input: keypressStream, escapeCodeTimeout: 0, @@ -834,7 +833,7 @@ export function KeypressProvider({ } return () => { - if (usePassthrough) { + if (keypressStream !== null) { keypressStream.removeListener('keypress', handleKeypress); stdin.removeListener('data', handleRawKeypress); } else { @@ -872,7 +871,7 @@ export function KeypressProvider({ } // Flush any pending paste data to avoid data loss on exit. - if (isPaste) { + if (pasteBuffer !== null) { broadcast({ name: '', ctrl: false, @@ -881,24 +880,20 @@ export function KeypressProvider({ paste: true, sequence: pasteBuffer.toString(), }); - pasteBuffer = Buffer.alloc(0); + pasteBuffer = null; } - if (draggingTimerRef.current) { - clearTimeout(draggingTimerRef.current); - draggingTimerRef.current = null; - } - if (isDraggingRef.current && dragBufferRef.current) { + clearDraggingTimer(); + if (dragBuffer) { broadcast({ name: '', ctrl: false, meta: false, shift: false, paste: true, - sequence: dragBufferRef.current, + sequence: dragBuffer, }); - isDraggingRef.current = false; - dragBufferRef.current = ''; + dragBuffer = ''; } }; }, [ @@ -906,8 +901,8 @@ export function KeypressProvider({ setRawMode, kittyProtocolEnabled, config, - subscribers, debugKeystrokeLogging, + broadcast, ]); return (