/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useEffect, useRef } from 'react'; import { useStdin } from 'ink'; import readline from 'readline'; export interface Key { name: string; ctrl: boolean; meta: boolean; shift: boolean; paste: boolean; sequence: string; } /** * A hook that listens for keypress events from stdin, providing a * key object that mirrors the one from Node's `readline` module, * adding a 'paste' flag for characters input as part of a bracketed * paste (when enabled). * * Pastes are currently sent as a single key event where the full paste * is in the sequence field. * * @param onKeypress - The callback function to execute on each keypress. * @param options - Options to control the hook's behavior. * @param options.isActive - Whether the hook should be actively listening for input. */ export function useKeypress( onKeypress: (key: Key) => void, { isActive }: { isActive: boolean }, ) { const { stdin, setRawMode } = useStdin(); const onKeypressRef = useRef(onKeypress); useEffect(() => { onKeypressRef.current = onKeypress; }, [onKeypress]); useEffect(() => { if (!isActive || !stdin.isTTY) { return; } setRawMode(true); const rl = readline.createInterface({ input: stdin }); let isPaste = false; let pasteBuffer = Buffer.alloc(0); const handleKeypress = (_: unknown, key: Key) => { if (key.name === 'paste-start') { isPaste = true; } else if (key.name === 'paste-end') { isPaste = false; onKeypressRef.current({ name: '', ctrl: false, meta: false, shift: false, paste: true, sequence: pasteBuffer.toString(), }); pasteBuffer = Buffer.alloc(0); } else { if (isPaste) { pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]); } else { // Handle special keys if (key.name === 'return' && key.sequence === '\x1B\r') { key.meta = true; } onKeypressRef.current({ ...key, paste: isPaste }); } } }; readline.emitKeypressEvents(stdin, rl); stdin.on('keypress', handleKeypress); return () => { stdin.removeListener('keypress', handleKeypress); rl.close(); setRawMode(false); // If we are in the middle of a paste, send what we have. if (isPaste) { onKeypressRef.current({ name: '', ctrl: false, meta: false, shift: false, paste: true, sequence: pasteBuffer.toString(), }); pasteBuffer = Buffer.alloc(0); } }; }, [isActive, stdin, setRawMode]); }