Skip to content

Commit 904e56b

Browse files
committed
add back fastreturn support
1 parent 8437ce9 commit 904e56b

4 files changed

Lines changed: 90 additions & 2 deletions

File tree

packages/cli/src/ui/components/InputPrompt.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
3838
import stripAnsi from 'strip-ansi';
3939
import chalk from 'chalk';
4040
import { StreamingState } from '../types.js';
41+
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
4142

4243
vi.mock('../hooks/useShellHistory.js');
4344
vi.mock('../hooks/useCommandCompletion.js');
@@ -124,6 +125,10 @@ describe('InputPrompt', () => {
124125

125126
beforeEach(() => {
126127
vi.resetAllMocks();
128+
vi.spyOn(
129+
terminalCapabilityManager,
130+
'isKittyProtocolEnabled',
131+
).mockReturnValue(true);
127132

128133
mockCommandContext = createMockCommandContext();
129134

packages/cli/src/ui/components/SettingsDialog.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ import {
3535
type SettingDefinition,
3636
type SettingsSchemaType,
3737
} from '../../config/settingsSchema.js';
38+
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
3839

3940
// Mock the VimModeContext
40-
const mockToggleVimEnabled = vi.fn();
41+
const mockToggleVimEnabled = vi.fn().mockResolvedValue(undefined);
4142
const mockSetVimMode = vi.fn();
4243

4344
vi.mock('../contexts/UIStateContext.js', () => ({
@@ -253,7 +254,12 @@ const renderDialog = (
253254

254255
describe('SettingsDialog', () => {
255256
beforeEach(() => {
256-
mockToggleVimEnabled.mockResolvedValue(true);
257+
vi.clearAllMocks();
258+
vi.spyOn(
259+
terminalCapabilityManager,
260+
'isKittyProtocolEnabled',
261+
).mockReturnValue(true);
262+
mockToggleVimEnabled.mockRejectedValue(undefined);
257263
});
258264

259265
afterEach(() => {

packages/cli/src/ui/contexts/KeypressContext.test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
KeypressProvider,
1717
useKeypressContext,
1818
ESC_TIMEOUT,
19+
FAST_RETURN_TIMEOUT,
1920
} from './KeypressContext.js';
21+
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
2022
import { useStdin } from 'ink';
2123
import { EventEmitter } from 'node:events';
2224

@@ -154,6 +156,53 @@ describe('KeypressContext', () => {
154156
);
155157
});
156158

159+
describe('Fast return buffering', () => {
160+
let kittySpy: ReturnType<typeof vi.spyOn>;
161+
162+
beforeEach(() => {
163+
kittySpy = vi
164+
.spyOn(terminalCapabilityManager, 'isKittyProtocolEnabled')
165+
.mockReturnValue(false);
166+
});
167+
168+
afterEach(() => kittySpy.mockRestore());
169+
170+
it('should buffer return key pressed quickly after another key', async () => {
171+
const { keyHandler } = setupKeypressTest();
172+
173+
act(() => stdin.write('a'));
174+
expect(keyHandler).toHaveBeenLastCalledWith(
175+
expect.objectContaining({ name: 'a' }),
176+
);
177+
178+
act(() => stdin.write('\r'));
179+
180+
expect(keyHandler).toHaveBeenLastCalledWith(
181+
expect.objectContaining({
182+
name: '',
183+
sequence: '\r',
184+
insertable: true,
185+
}),
186+
);
187+
});
188+
189+
it('should NOT buffer return key if delay is long enough', async () => {
190+
const { keyHandler } = setupKeypressTest();
191+
192+
act(() => stdin.write('a'));
193+
194+
vi.advanceTimersByTime(FAST_RETURN_TIMEOUT + 1);
195+
196+
act(() => stdin.write('\r'));
197+
198+
expect(keyHandler).toHaveBeenLastCalledWith(
199+
expect.objectContaining({
200+
name: 'return',
201+
}),
202+
);
203+
});
204+
});
205+
157206
describe('Escape key handling', () => {
158207
it('should recognize escape key (keycode 27) in kitty protocol', async () => {
159208
const { keyHandler } = setupKeypressTest();

packages/cli/src/ui/contexts/KeypressContext.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ESC } from '../utils/input.js';
1919
import { parseMouseEvent } from '../utils/mouse.js';
2020
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
2121
import { appEvents, AppEvent } from '../../utils/events.js';
22+
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
2223

2324
export const BACKSLASH_ENTER_TIMEOUT = 5;
2425
export const ESC_TIMEOUT = 50;
@@ -143,6 +144,30 @@ function nonKeyboardEventFilter(
143144
};
144145
}
145146

147+
/**
148+
* Converts return keys pressed quickly after other keys into plain
149+
* insertable return characters.
150+
*
151+
* This is to accommodate older terminals that paste text without bracketing.
152+
*/
153+
function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler {
154+
let lastKeyTime = 0;
155+
return (key: Key) => {
156+
const now = Date.now();
157+
if (key.name === 'return' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) {
158+
keypressHandler({
159+
...key,
160+
name: '',
161+
sequence: '\r',
162+
insertable: true,
163+
});
164+
} else {
165+
keypressHandler(key);
166+
}
167+
lastKeyTime = now;
168+
};
169+
}
170+
146171
/**
147172
* Buffers "/" keys to see if they are followed return.
148173
* Will flush the buffer if no data is received for DRAG_COMPLETION_TIMEOUT_MS
@@ -641,6 +666,9 @@ export function KeypressProvider({
641666
process.stdin.setEncoding('utf8'); // Make data events emit strings
642667

643668
let processor = nonKeyboardEventFilter(broadcast);
669+
if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
670+
processor = bufferFastReturn(processor);
671+
}
644672
processor = bufferBackslashEnter(processor);
645673
processor = bufferPaste(processor);
646674
let dataListener = createDataListener(processor);

0 commit comments

Comments
 (0)