対話入力時Ctrl+Cが聞くように。
This commit is contained in:
parent
69eb9e8d3d
commit
c5b3f992db
@ -548,6 +548,18 @@ describe('interactiveMode', () => {
|
|||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle Ctrl+C (Kitty CSI-u) to cancel input', async () => {
|
||||||
|
// Given: Ctrl+C reported as Kitty keyboard protocol sequence
|
||||||
|
setupRawStdin(['\x1B[99;5u']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: should cancel
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle Ctrl+D to cancel input', async () => {
|
it('should handle Ctrl+D to cancel input', async () => {
|
||||||
// Given: Ctrl+D during input
|
// Given: Ctrl+D during input
|
||||||
setupRawStdin(['\x04']);
|
setupRawStdin(['\x04']);
|
||||||
@ -560,6 +572,42 @@ describe('interactiveMode', () => {
|
|||||||
expect(result.action).toBe('cancel');
|
expect(result.action).toBe('cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle Ctrl+D (Kitty CSI-u) to cancel input', async () => {
|
||||||
|
// Given: Ctrl+D reported as Kitty keyboard protocol sequence
|
||||||
|
setupRawStdin(['\x1B[100;5u']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: should cancel
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Ctrl+C (xterm modifyOtherKeys) to cancel input', async () => {
|
||||||
|
// Given: Ctrl+C reported as xterm modifyOtherKeys sequence
|
||||||
|
setupRawStdin(['\x1B[27;5;99~']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: should cancel
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Ctrl+D (xterm modifyOtherKeys) to cancel input', async () => {
|
||||||
|
// Given: Ctrl+D reported as xterm modifyOtherKeys sequence
|
||||||
|
setupRawStdin(['\x1B[27;5;100~']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: should cancel
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
it('should ignore arrow keys in normal mode', async () => {
|
it('should ignore arrow keys in normal mode', async () => {
|
||||||
// Given: text with arrow keys interspersed (arrows are ignored)
|
// Given: text with arrow keys interspersed (arrows are ignored)
|
||||||
setupRawStdin([
|
setupRawStdin([
|
||||||
|
|||||||
@ -190,6 +190,45 @@ const ESC_SHIFT_ENTER = '[13;2u';
|
|||||||
|
|
||||||
type InputState = 'normal' | 'paste';
|
type InputState = 'normal' | 'paste';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode Kitty CSI-u key sequence into a control character.
|
||||||
|
* Example: "[99;5u" (Ctrl+C) -> "\x03"
|
||||||
|
*/
|
||||||
|
function decodeCtrlKey(rest: string): { ch: string; consumed: number } | null {
|
||||||
|
// Kitty CSI-u: [codepoint;modifiersu
|
||||||
|
const kittyMatch = rest.match(/^\[(\d+);(\d+)u/);
|
||||||
|
if (kittyMatch) {
|
||||||
|
const codepoint = Number.parseInt(kittyMatch[1]!, 10);
|
||||||
|
const modifiers = Number.parseInt(kittyMatch[2]!, 10);
|
||||||
|
// Kitty modifiers are 1-based; Ctrl bit is 4 in 0-based flags.
|
||||||
|
const ctrlPressed = (((modifiers - 1) & 4) !== 0);
|
||||||
|
if (!ctrlPressed) return null;
|
||||||
|
|
||||||
|
const key = String.fromCodePoint(codepoint);
|
||||||
|
if (!/^[A-Za-z]$/.test(key)) return null;
|
||||||
|
|
||||||
|
const upper = key.toUpperCase();
|
||||||
|
const controlCode = upper.charCodeAt(0) & 0x1f;
|
||||||
|
return { ch: String.fromCharCode(controlCode), consumed: kittyMatch[0].length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// xterm modifyOtherKeys: [27;modifiers;codepoint~
|
||||||
|
const xtermMatch = rest.match(/^\[27;(\d+);(\d+)~/);
|
||||||
|
if (!xtermMatch) return null;
|
||||||
|
|
||||||
|
const modifiers = Number.parseInt(xtermMatch[1]!, 10);
|
||||||
|
const codepoint = Number.parseInt(xtermMatch[2]!, 10);
|
||||||
|
const ctrlPressed = (((modifiers - 1) & 4) !== 0);
|
||||||
|
if (!ctrlPressed) return null;
|
||||||
|
|
||||||
|
const key = String.fromCodePoint(codepoint);
|
||||||
|
if (!/^[A-Za-z]$/.test(key)) return null;
|
||||||
|
|
||||||
|
const upper = key.toUpperCase();
|
||||||
|
const controlCode = upper.charCodeAt(0) & 0x1f;
|
||||||
|
return { ch: String.fromCharCode(controlCode), consumed: xtermMatch[0].length };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse raw stdin data and process each character/sequence.
|
* Parse raw stdin data and process each character/sequence.
|
||||||
*
|
*
|
||||||
@ -229,6 +268,12 @@ function parseInputData(
|
|||||||
i += 1 + ESC_SHIFT_ENTER.length;
|
i += 1 + ESC_SHIFT_ENTER.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const ctrlKey = decodeCtrlKey(rest);
|
||||||
|
if (ctrlKey) {
|
||||||
|
callbacks.onChar(ctrlKey.ch);
|
||||||
|
i += 1 + ctrlKey.consumed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Arrow keys and other CSI sequences: skip \x1B[ + letter/params
|
// Arrow keys and other CSI sequences: skip \x1B[ + letter/params
|
||||||
if (rest.startsWith('[')) {
|
if (rest.startsWith('[')) {
|
||||||
const csiMatch = rest.match(/^\[[0-9;]*[A-Za-z~]/);
|
const csiMatch = rest.match(/^\[[0-9;]*[A-Za-z~]/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user