summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/contexts/KeypressContext.test.tsx307
-rw-r--r--packages/cli/src/ui/contexts/KeypressContext.tsx7
-rw-r--r--packages/cli/src/ui/utils/platformConstants.ts6
3 files changed, 319 insertions, 1 deletions
diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
new file mode 100644
index 00000000..01630229
--- /dev/null
+++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
@@ -0,0 +1,307 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { renderHook, act } from '@testing-library/react';
+import { vi, Mock } from 'vitest';
+import {
+ KeypressProvider,
+ useKeypressContext,
+ Key,
+} from './KeypressContext.js';
+import { useStdin } from 'ink';
+import { EventEmitter } from 'events';
+import {
+ KITTY_KEYCODE_ENTER,
+ KITTY_KEYCODE_NUMPAD_ENTER,
+} from '../utils/platformConstants.js';
+
+// Mock the 'ink' module to control stdin
+vi.mock('ink', async (importOriginal) => {
+ const original = await importOriginal<typeof import('ink')>();
+ return {
+ ...original,
+ useStdin: vi.fn(),
+ };
+});
+
+// Mock the 'readline' module
+vi.mock('readline', () => {
+ const mockedReadline = {
+ createInterface: vi.fn().mockReturnValue({ close: vi.fn() }),
+ emitKeypressEvents: vi.fn(),
+ };
+ return {
+ ...mockedReadline,
+ default: mockedReadline,
+ };
+});
+
+class MockStdin extends EventEmitter {
+ isTTY = true;
+ setRawMode = vi.fn();
+ override on = this.addListener;
+ override removeListener = super.removeListener;
+ write = vi.fn();
+ resume = vi.fn();
+
+ // Helper to simulate a keypress event
+ pressKey(key: Partial<Key>) {
+ this.emit('keypress', null, key);
+ }
+
+ // Helper to simulate a kitty protocol sequence
+ sendKittySequence(sequence: string) {
+ // Kitty sequences come in multiple parts
+ // For example, ESC[13u comes as: ESC[ then 1 then 3 then u
+ // ESC[57414;2u comes as: ESC[ then 5 then 7 then 4 then 1 then 4 then ; then 2 then u
+ const escIndex = sequence.indexOf('\x1b[');
+ if (escIndex !== -1) {
+ // Send ESC[
+ this.emit('keypress', null, {
+ name: undefined,
+ sequence: '\x1b[',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ });
+
+ // Send the rest character by character
+ const rest = sequence.substring(escIndex + 2);
+ for (const char of rest) {
+ this.emit('keypress', null, {
+ name: /[a-zA-Z0-9]/.test(char) ? char : undefined,
+ sequence: char,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ });
+ }
+ }
+ }
+}
+
+describe('KeypressContext - Kitty Protocol', () => {
+ let stdin: MockStdin;
+ const mockSetRawMode = vi.fn();
+
+ const wrapper = ({
+ children,
+ kittyProtocolEnabled = true,
+ }: {
+ children: React.ReactNode;
+ kittyProtocolEnabled?: boolean;
+ }) => (
+ <KeypressProvider kittyProtocolEnabled={kittyProtocolEnabled}>
+ {children}
+ </KeypressProvider>
+ );
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ stdin = new MockStdin();
+ (useStdin as Mock).mockReturnValue({
+ stdin,
+ setRawMode: mockSetRawMode,
+ });
+ });
+
+ describe('Enter key handling', () => {
+ it('should recognize regular enter key (keycode 13) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for regular enter: ESC[13u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_ENTER}u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should recognize numpad enter key (keycode 57414) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter: ESC[57414u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER}u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with modifiers', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Shift (modifier 2): ESC[57414;2u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};2u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: true,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with Ctrl modifier', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};5u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: true,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with Alt modifier', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Alt (modifier 3): ESC[57414;3u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};3u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: true,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should not process kitty sequences when kitty protocol is disabled', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: false }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER}u`);
+ });
+
+ // When kitty protocol is disabled, the sequence should be passed through
+ // as individual keypresses, not recognized as a single enter key
+ expect(keyHandler).not.toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ }),
+ );
+ });
+ });
+
+ describe('Escape key handling', () => {
+ it('should recognize escape key (keycode 27) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for escape: ESC[27u
+ act(() => {
+ stdin.sendKittySequence('\x1b[27u');
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'escape',
+ kittyProtocol: true,
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index f0c000a0..b1e70aed 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -22,6 +22,8 @@ import { PassThrough } from 'stream';
import {
BACKSLASH_ENTER_DETECTION_WINDOW_MS,
KITTY_CTRL_C,
+ KITTY_KEYCODE_ENTER,
+ KITTY_KEYCODE_NUMPAD_ENTER,
MAX_KITTY_SEQUENCE_LENGTH,
} from '../utils/platformConstants.js';
@@ -132,7 +134,10 @@ export function KeypressProvider({
};
}
- if (keyCode === 13) {
+ if (
+ keyCode === KITTY_KEYCODE_ENTER ||
+ keyCode === KITTY_KEYCODE_NUMPAD_ENTER
+ ) {
return {
name: 'return',
ctrl,
diff --git a/packages/cli/src/ui/utils/platformConstants.ts b/packages/cli/src/ui/utils/platformConstants.ts
index 9d2e1990..8aff581e 100644
--- a/packages/cli/src/ui/utils/platformConstants.ts
+++ b/packages/cli/src/ui/utils/platformConstants.ts
@@ -18,6 +18,12 @@
export const KITTY_CTRL_C = '[99;5u';
/**
+ * Kitty keyboard protocol keycodes
+ */
+export const KITTY_KEYCODE_ENTER = 13;
+export const KITTY_KEYCODE_NUMPAD_ENTER = 57414;
+
+/**
* Timing constants for terminal interactions
*/
export const CTRL_EXIT_PROMPT_DURATION_MS = 1000;