105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
|
|
/**
|
||
|
|
* @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]);
|
||
|
|
}
|