diff options
Diffstat (limited to 'packages/cli/src/ui/hooks/useKeypress.ts')
| -rw-r--r-- | packages/cli/src/ui/hooks/useKeypress.ts | 88 |
1 files changed, 84 insertions, 4 deletions
diff --git a/packages/cli/src/ui/hooks/useKeypress.ts b/packages/cli/src/ui/hooks/useKeypress.ts index a8adba8d..d3e3df5c 100644 --- a/packages/cli/src/ui/hooks/useKeypress.ts +++ b/packages/cli/src/ui/hooks/useKeypress.ts @@ -7,6 +7,7 @@ import { useEffect, useRef } from 'react'; import { useStdin } from 'ink'; import readline from 'readline'; +import { PassThrough } from 'stream'; export interface Key { name: string; @@ -48,7 +49,19 @@ export function useKeypress( setRawMode(true); - const rl = readline.createInterface({ input: stdin }); + 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' + ) { + // Prior to node 20, node's built-in readline does not support bracketed + // paste mode. We hack by detecting it with our own handler. + usePassthrough = true; + } + let isPaste = false; let pasteBuffer = Buffer.alloc(0); @@ -79,11 +92,78 @@ export function useKeypress( } }; - readline.emitKeypressEvents(stdin, rl); - stdin.on('keypress', handleKeypress); + const handleRawKeypress = (data: Buffer) => { + const PASTE_MODE_PREFIX = Buffer.from('\x1B[200~'); + const PASTE_MODE_SUFFIX = Buffer.from('\x1B[201~'); + + let pos = 0; + while (pos < data.length) { + const prefixPos = data.indexOf(PASTE_MODE_PREFIX, pos); + const suffixPos = data.indexOf(PASTE_MODE_SUFFIX, pos); + + // Determine which marker comes first, if any. + const isPrefixNext = + prefixPos !== -1 && (suffixPos === -1 || prefixPos < suffixPos); + const isSuffixNext = + suffixPos !== -1 && (prefixPos === -1 || suffixPos < prefixPos); + + let nextMarkerPos = -1; + let markerLength = 0; + + if (isPrefixNext) { + nextMarkerPos = prefixPos; + } else if (isSuffixNext) { + nextMarkerPos = suffixPos; + } + markerLength = PASTE_MODE_SUFFIX.length; + + if (nextMarkerPos === -1) { + keypressStream.write(data.slice(pos)); + return; + } + + const nextData = data.slice(pos, nextMarkerPos); + if (nextData.length > 0) { + keypressStream.write(nextData); + } + const createPasteKeyEvent = ( + name: 'paste-start' | 'paste-end', + ): Key => ({ + name, + ctrl: false, + meta: false, + shift: false, + paste: false, + sequence: '', + }); + if (isPrefixNext) { + handleKeypress(undefined, createPasteKeyEvent('paste-start')); + } else if (isSuffixNext) { + handleKeypress(undefined, createPasteKeyEvent('paste-end')); + } + pos = nextMarkerPos + markerLength; + } + }; + + let rl: readline.Interface; + if (usePassthrough) { + rl = readline.createInterface({ input: keypressStream }); + readline.emitKeypressEvents(keypressStream, rl); + keypressStream.on('keypress', handleKeypress); + stdin.on('data', handleRawKeypress); + } else { + rl = readline.createInterface({ input: stdin }); + readline.emitKeypressEvents(stdin, rl); + stdin.on('keypress', handleKeypress); + } return () => { - stdin.removeListener('keypress', handleKeypress); + if (usePassthrough) { + keypressStream.removeListener('keypress', handleKeypress); + stdin.removeListener('data', handleRawKeypress); + } else { + stdin.removeListener('keypress', handleKeypress); + } rl.close(); setRawMode(false); |
