mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-17 01:21:10 -07:00
feat(cli): support literal character keybindings and extended Kitty protocol keys (#21972)
This commit is contained in:
committed by
GitHub
parent
f8ad3a200a
commit
075e0b1a81
@@ -637,6 +637,9 @@ describe('KeypressContext', () => {
|
||||
|
||||
describe('Parameterized functional keys', () => {
|
||||
it.each([
|
||||
// CSI-u numeric keys
|
||||
{ sequence: `\x1b[53;5u`, expected: { name: '5', ctrl: true } },
|
||||
{ sequence: `\x1b[51;2u`, expected: { name: '3', shift: true } },
|
||||
// ModifyOtherKeys
|
||||
{ sequence: `\x1b[27;2;13~`, expected: { name: 'enter', shift: true } },
|
||||
{ sequence: `\x1b[27;5;13~`, expected: { name: 'enter', ctrl: true } },
|
||||
@@ -665,6 +668,14 @@ describe('KeypressContext', () => {
|
||||
{ sequence: `\x1b[17~`, expected: { name: 'f6' } },
|
||||
{ sequence: `\x1b[23~`, expected: { name: 'f11' } },
|
||||
{ sequence: `\x1b[24~`, expected: { name: 'f12' } },
|
||||
{ sequence: `\x1b[25~`, expected: { name: 'f13' } },
|
||||
{ sequence: `\x1b[34~`, expected: { name: 'f20' } },
|
||||
// Kitty Extended Function Keys (F13-F35)
|
||||
{ sequence: `\x1b[302u`, expected: { name: 'f13' } },
|
||||
{ sequence: `\x1b[324u`, expected: { name: 'f35' } },
|
||||
// Modifier / Special Keys (Kitty Protocol)
|
||||
{ sequence: `\x1b[57358u`, expected: { name: 'capslock' } },
|
||||
{ sequence: `\x1b[57362u`, expected: { name: 'pausebreak' } },
|
||||
// Reverse tabs
|
||||
{ sequence: `\x1b[Z`, expected: { name: 'tab', shift: true } },
|
||||
{ sequence: `\x1b[1;2Z`, expected: { name: 'tab', shift: true } },
|
||||
@@ -820,6 +831,20 @@ describe('KeypressContext', () => {
|
||||
sequence: '\x1bOn',
|
||||
expected: { name: '.', sequence: '.', insertable: true },
|
||||
},
|
||||
// Kitty Numpad Support (CSI-u)
|
||||
{
|
||||
sequence: '\x1b[57404u',
|
||||
expected: { name: 'numpad5', sequence: '5', insertable: true },
|
||||
},
|
||||
{
|
||||
modifier: 'Ctrl',
|
||||
sequence: '\x1b[57404;5u',
|
||||
expected: { name: 'numpad5', ctrl: true, insertable: false },
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[57411u',
|
||||
expected: { name: 'numpad_multiply', sequence: '*', insertable: true },
|
||||
},
|
||||
])(
|
||||
'should recognize numpad sequence "$sequence" as $expected.name',
|
||||
({ sequence, expected }) => {
|
||||
|
||||
@@ -66,6 +66,14 @@ const KEY_INFO_MAP: Record<
|
||||
'[21~': { name: 'f10' },
|
||||
'[23~': { name: 'f11' },
|
||||
'[24~': { name: 'f12' },
|
||||
'[25~': { name: 'f13' },
|
||||
'[26~': { name: 'f14' },
|
||||
'[28~': { name: 'f15' },
|
||||
'[29~': { name: 'f16' },
|
||||
'[31~': { name: 'f17' },
|
||||
'[32~': { name: 'f18' },
|
||||
'[33~': { name: 'f19' },
|
||||
'[34~': { name: 'f20' },
|
||||
'[A': { name: 'up' },
|
||||
'[B': { name: 'down' },
|
||||
'[C': { name: 'right' },
|
||||
@@ -91,12 +99,6 @@ const KEY_INFO_MAP: Record<
|
||||
OZ: { name: 'tab', shift: true }, // SS3 Shift+Tab variant for Windows terminals
|
||||
'[[5~': { name: 'pageup' },
|
||||
'[[6~': { name: 'pagedown' },
|
||||
'[9u': { name: 'tab' },
|
||||
'[13u': { name: 'enter' },
|
||||
'[27u': { name: 'escape' },
|
||||
'[32u': { name: 'space' },
|
||||
'[127u': { name: 'backspace' },
|
||||
'[57414u': { name: 'enter' }, // Numpad Enter
|
||||
'[a': { name: 'up', shift: true },
|
||||
'[b': { name: 'down', shift: true },
|
||||
'[c': { name: 'right', shift: true },
|
||||
@@ -122,6 +124,46 @@ const KEY_INFO_MAP: Record<
|
||||
'[8^': { name: 'end', ctrl: true },
|
||||
};
|
||||
|
||||
// Kitty Keyboard Protocol (CSI u) code mappings
|
||||
const KITTY_CODE_MAP: Record<number, { name: string; sequence?: string }> = {
|
||||
2: { name: 'insert' },
|
||||
3: { name: 'delete' },
|
||||
5: { name: 'pageup' },
|
||||
6: { name: 'pagedown' },
|
||||
9: { name: 'tab' },
|
||||
13: { name: 'enter' },
|
||||
14: { name: 'up' },
|
||||
15: { name: 'down' },
|
||||
16: { name: 'right' },
|
||||
17: { name: 'left' },
|
||||
27: { name: 'escape' },
|
||||
32: { name: 'space', sequence: ' ' },
|
||||
127: { name: 'backspace' },
|
||||
57358: { name: 'capslock' },
|
||||
57359: { name: 'scrolllock' },
|
||||
57360: { name: 'numlock' },
|
||||
57361: { name: 'printscreen' },
|
||||
57362: { name: 'pausebreak' },
|
||||
57409: { name: 'numpad_decimal', sequence: '.' },
|
||||
57410: { name: 'numpad_divide', sequence: '/' },
|
||||
57411: { name: 'numpad_multiply', sequence: '*' },
|
||||
57412: { name: 'numpad_subtract', sequence: '-' },
|
||||
57413: { name: 'numpad_add', sequence: '+' },
|
||||
57414: { name: 'enter' },
|
||||
57416: { name: 'numpad_separator', sequence: ',' },
|
||||
// Function keys F13-F35, not standard, but supported by Kitty
|
||||
...Object.fromEntries(
|
||||
Array.from({ length: 23 }, (_, i) => [302 + i, { name: `f${13 + i}` }]),
|
||||
),
|
||||
// Numpad keys in Numeric Keypad Mode (CSI u codes 57399-57408)
|
||||
...Object.fromEntries(
|
||||
Array.from({ length: 10 }, (_, i) => [
|
||||
57399 + i,
|
||||
{ name: `numpad${i}`, sequence: String(i) },
|
||||
]),
|
||||
),
|
||||
};
|
||||
|
||||
// Numpad keys in Application Keypad Mode (SS3 sequences)
|
||||
const NUMPAD_MAP: Record<string, string> = {
|
||||
Oj: '*',
|
||||
@@ -565,17 +607,24 @@ function* emitKeys(
|
||||
}
|
||||
} else {
|
||||
name = 'undefined';
|
||||
if (
|
||||
(ctrl || cmd || alt) &&
|
||||
(code.endsWith('u') || code.endsWith('~'))
|
||||
) {
|
||||
if (code.endsWith('u') || code.endsWith('~')) {
|
||||
// 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);
|
||||
if (codeNumber >= 33 && codeNumber <= 126) {
|
||||
const char = String.fromCharCode(codeNumber);
|
||||
name = char.toLowerCase();
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
shift = true;
|
||||
}
|
||||
} else {
|
||||
const mapped = KITTY_CODE_MAP[codeNumber];
|
||||
if (mapped) {
|
||||
name = mapped.name;
|
||||
if (mapped.sequence && !ctrl && !cmd && !alt) {
|
||||
sequence = mapped.sequence;
|
||||
insertable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +97,6 @@ describe('KeyBinding', () => {
|
||||
'Invalid keybinding key: "ctlr+a" in "ctlr+a"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error for literal "+" as key (must use "=")', () => {
|
||||
// VS Code style peeling logic results in "+" as the remains
|
||||
expect(() => new KeyBinding('alt++')).toThrow(
|
||||
'Invalid keybinding key: "+" in "alt++"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -110,10 +110,8 @@ export enum Command {
|
||||
* Data-driven key binding structure for user configuration
|
||||
*/
|
||||
export class KeyBinding {
|
||||
private static readonly VALID_KEYS = new Set([
|
||||
...'abcdefghijklmnopqrstuvwxyz0123456789', // Letters & Numbers
|
||||
..."`-=[]\\;',./", // Punctuation
|
||||
...Array.from({ length: 19 }, (_, i) => `f${i + 1}`), // Function Keys
|
||||
private static readonly VALID_LONG_KEYS = new Set([
|
||||
...Array.from({ length: 35 }, (_, i) => `f${i + 1}`), // Function Keys
|
||||
...Array.from({ length: 10 }, (_, i) => `numpad${i}`), // Numpad Numbers
|
||||
// Navigation & Actions
|
||||
'left',
|
||||
@@ -130,6 +128,7 @@ export class KeyBinding {
|
||||
'space',
|
||||
'backspace',
|
||||
'delete',
|
||||
'clear',
|
||||
'pausebreak',
|
||||
'capslock',
|
||||
'insert',
|
||||
@@ -193,8 +192,11 @@ export class KeyBinding {
|
||||
|
||||
const key = remains;
|
||||
|
||||
if (!KeyBinding.VALID_KEYS.has(key)) {
|
||||
throw new Error(`Invalid keybinding key: "${key}" in "${pattern}"`);
|
||||
if ([...key].length !== 1 && !KeyBinding.VALID_LONG_KEYS.has(key)) {
|
||||
throw new Error(
|
||||
`Invalid keybinding key: "${key}" in "${pattern}".` +
|
||||
` Must be a single character or one of: ${[...KeyBinding.VALID_LONG_KEYS].join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
|
||||
Reference in New Issue
Block a user