feat(cli): add support for numpad SS3 sequences (#19659)

This commit is contained in:
Tommaso Sciortino
2026-02-20 10:09:10 -08:00
committed by GitHub
parent 27b7fc04de
commit d54702185b
2 changed files with 112 additions and 10 deletions

View File

@@ -758,6 +758,80 @@ describe('KeypressContext', () => {
);
});
describe('Numpad support', () => {
it.each([
{
sequence: '\x1bOj',
expected: { name: '*', sequence: '*', insertable: true },
},
{
sequence: '\x1bOk',
expected: { name: '+', sequence: '+', insertable: true },
},
{
sequence: '\x1bOm',
expected: { name: '-', sequence: '-', insertable: true },
},
{
sequence: '\x1bOo',
expected: { name: '/', sequence: '/', insertable: true },
},
{
sequence: '\x1bOp',
expected: { name: '0', sequence: '0', insertable: true },
},
{
sequence: '\x1bOq',
expected: { name: '1', sequence: '1', insertable: true },
},
{
sequence: '\x1bOr',
expected: { name: '2', sequence: '2', insertable: true },
},
{
sequence: '\x1bOs',
expected: { name: '3', sequence: '3', insertable: true },
},
{
sequence: '\x1bOt',
expected: { name: '4', sequence: '4', insertable: true },
},
{
sequence: '\x1bOu',
expected: { name: '5', sequence: '5', insertable: true },
},
{
sequence: '\x1bOv',
expected: { name: '6', sequence: '6', insertable: true },
},
{
sequence: '\x1bOw',
expected: { name: '7', sequence: '7', insertable: true },
},
{
sequence: '\x1bOx',
expected: { name: '8', sequence: '8', insertable: true },
},
{
sequence: '\x1bOy',
expected: { name: '9', sequence: '9', insertable: true },
},
{
sequence: '\x1bOn',
expected: { name: '.', sequence: '.', insertable: true },
},
])(
'should recognize numpad sequence "$sequence" as $expected.name',
({ sequence, expected }) => {
const { keyHandler } = setupKeypressTest();
act(() => stdin.write(sequence));
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining(expected),
);
},
);
});
describe('Double-tap and batching', () => {
it('should emit two delete events for double-tap CSI[3~', async () => {
const { keyHandler } = setupKeypressTest();

View File

@@ -122,6 +122,25 @@ const KEY_INFO_MAP: Record<
'[8^': { name: 'end', ctrl: true },
};
// Numpad keys in Application Keypad Mode (SS3 sequences)
const NUMPAD_MAP: Record<string, string> = {
Oj: '*',
Ok: '+',
Om: '-',
Oo: '/',
Op: '0',
Oq: '1',
Or: '2',
Os: '3',
Ot: '4',
Ou: '5',
Ov: '6',
Ow: '7',
Ox: '8',
Oy: '9',
On: '.',
};
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
function charLengthAt(str: string, i: number): number {
if (str.length <= i) {
@@ -538,18 +557,27 @@ function* emitKeys(
insertable = true;
}
} else {
name = 'undefined';
if (
(ctrl || cmd || alt) &&
(code.endsWith('u') || code.endsWith('~'))
) {
// CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
const codeNumber = parseInt(code.slice(1, -1), 10);
const numpadChar = NUMPAD_MAP[code];
if (numpadChar) {
name = numpadChar;
if (!ctrl && !cmd && !alt) {
sequence = numpadChar;
insertable = true;
}
} else {
name = 'undefined';
if (
codeNumber >= 'a'.charCodeAt(0) &&
codeNumber <= 'z'.charCodeAt(0)
(ctrl || cmd || alt) &&
(code.endsWith('u') || code.endsWith('~'))
) {
name = String.fromCharCode(codeNumber);
// CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
const codeNumber = parseInt(code.slice(1, -1), 10);
if (
codeNumber >= 'a'.charCodeAt(0) &&
codeNumber <= 'z'.charCodeAt(0)
) {
name = String.fromCharCode(codeNumber);
}
}
}
}