対話入力時Ctrl+Cが聞くように。

This commit is contained in:
nrslib 2026-02-08 07:14:55 +09:00
parent 69eb9e8d3d
commit c5b3f992db
2 changed files with 93 additions and 0 deletions

View File

@ -548,6 +548,18 @@ describe('interactiveMode', () => {
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 () => {
// Given: Ctrl+D during input
setupRawStdin(['\x04']);
@ -560,6 +572,42 @@ describe('interactiveMode', () => {
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 () => {
// Given: text with arrow keys interspersed (arrows are ignored)
setupRawStdin([

View File

@ -190,6 +190,45 @@ const ESC_SHIFT_ENTER = '[13;2u';
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.
*
@ -229,6 +268,12 @@ function parseInputData(
i += 1 + ESC_SHIFT_ENTER.length;
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
if (rest.startsWith('[')) {
const csiMatch = rest.match(/^\[[0-9;]*[A-Za-z~]/);