summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/hooks')
-rw-r--r--packages/cli/src/ui/hooks/useBracketedPaste.ts37
-rw-r--r--packages/cli/src/ui/hooks/useKeypress.ts104
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]);
+}