diff options
Diffstat (limited to 'packages/cli/src/ui/hooks')
| -rw-r--r-- | packages/cli/src/ui/hooks/useBracketedPaste.ts | 37 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useKeypress.ts | 104 |
2 files changed, 141 insertions, 0 deletions
diff --git a/packages/cli/src/ui/hooks/useBracketedPaste.ts b/packages/cli/src/ui/hooks/useBracketedPaste.ts new file mode 100644 index 00000000..ae58be3b --- /dev/null +++ b/packages/cli/src/ui/hooks/useBracketedPaste.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect } from 'react'; + +const ENABLE_BRACKETED_PASTE = '\x1b[?2004h'; +const DISABLE_BRACKETED_PASTE = '\x1b[?2004l'; + +/** + * Enables and disables bracketed paste mode in the terminal. + * + * This hook ensures that bracketed paste mode is enabled when the component + * mounts and disabled when it unmounts or when the process exits. + */ +export const useBracketedPaste = () => { + const cleanup = () => { + process.stdout.write(DISABLE_BRACKETED_PASTE); + }; + + useEffect(() => { + process.stdout.write(ENABLE_BRACKETED_PASTE); + + process.on('exit', cleanup); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + + return () => { + cleanup(); + process.removeListener('exit', cleanup); + process.removeListener('SIGINT', cleanup); + process.removeListener('SIGTERM', cleanup); + }; + }, []); +}; diff --git a/packages/cli/src/ui/hooks/useKeypress.ts b/packages/cli/src/ui/hooks/useKeypress.ts new file mode 100644 index 00000000..a8adba8d --- /dev/null +++ b/packages/cli/src/ui/hooks/useKeypress.ts @@ -0,0 +1,104 @@ +/** + * @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]); +} |
