mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Handle stdin for prompts using readline for escape character parsing (#1972)
This commit is contained in:
37
packages/cli/src/ui/hooks/useBracketedPaste.ts
Normal file
37
packages/cli/src/ui/hooks/useBracketedPaste.ts
Normal file
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
104
packages/cli/src/ui/hooks/useKeypress.ts
Normal file
104
packages/cli/src/ui/hooks/useKeypress.ts
Normal file
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user