mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Added modifyOtherKeys protocol support for tmux (#15524)
Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
This commit is contained in:
@@ -21,6 +21,8 @@ vi.mock('@google/gemini-cli-core', () => ({
|
|||||||
},
|
},
|
||||||
enableKittyKeyboardProtocol: vi.fn(),
|
enableKittyKeyboardProtocol: vi.fn(),
|
||||||
disableKittyKeyboardProtocol: vi.fn(),
|
disableKittyKeyboardProtocol: vi.fn(),
|
||||||
|
enableModifyOtherKeys: vi.fn(),
|
||||||
|
disableModifyOtherKeys: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('TerminalCapabilityManager', () => {
|
describe('TerminalCapabilityManager', () => {
|
||||||
@@ -174,4 +176,92 @@ describe('TerminalCapabilityManager', () => {
|
|||||||
await promise;
|
await promise;
|
||||||
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('modifyOtherKeys detection', () => {
|
||||||
|
it('should detect modifyOtherKeys support (level 2)', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
// Simulate modifyOtherKeys level 2 response: \x1b[>4;2m
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not enable modifyOtherKeys for level 0', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
// Simulate modifyOtherKeys level 0 response: \x1b[>4;0m
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;0m'));
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer Kitty over modifyOtherKeys', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
// Simulate both Kitty and modifyOtherKeys responses
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?1u'));
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable modifyOtherKeys when Kitty not supported', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
// Simulate only modifyOtherKeys response (no Kitty)
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(true);
|
||||||
|
expect(manager.isKittyProtocolEnabled()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle split modifyOtherKeys response chunks', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
// Split response: \x1b[>4;2m
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;'));
|
||||||
|
stdin.emit('data', Buffer.from('2m'));
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect modifyOtherKeys with other capabilities', async () => {
|
||||||
|
const manager = TerminalCapabilityManager.getInstance();
|
||||||
|
const promise = manager.detectCapabilities();
|
||||||
|
|
||||||
|
stdin.emit('data', Buffer.from('\x1b]11;rgb:1a1a/1a1a/1a1a\x1b\\')); // background color
|
||||||
|
stdin.emit('data', Buffer.from('\x1bP>|tmux\x1b\\')); // Terminal name
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[>4;2m')); // modifyOtherKeys
|
||||||
|
// Complete detection with DA1
|
||||||
|
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a');
|
||||||
|
expect(manager.getTerminalName()).toBe('tmux');
|
||||||
|
expect(manager.isModifyOtherKeysEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
debugLogger,
|
debugLogger,
|
||||||
enableKittyKeyboardProtocol,
|
enableKittyKeyboardProtocol,
|
||||||
disableKittyKeyboardProtocol,
|
disableKittyKeyboardProtocol,
|
||||||
|
enableModifyOtherKeys,
|
||||||
|
disableModifyOtherKeys,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export type TerminalBackgroundColor = string | undefined;
|
export type TerminalBackgroundColor = string | undefined;
|
||||||
@@ -20,6 +22,7 @@ export class TerminalCapabilityManager {
|
|||||||
private static readonly OSC_11_QUERY = '\x1b]11;?\x1b\\';
|
private static readonly OSC_11_QUERY = '\x1b]11;?\x1b\\';
|
||||||
private static readonly TERMINAL_NAME_QUERY = '\x1b[>q';
|
private static readonly TERMINAL_NAME_QUERY = '\x1b[>q';
|
||||||
private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c';
|
private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c';
|
||||||
|
private static readonly MODIFY_OTHER_KEYS_QUERY = '\x1b[>4;?m';
|
||||||
|
|
||||||
// Kitty keyboard flags: CSI ? flags u
|
// Kitty keyboard flags: CSI ? flags u
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
@@ -34,12 +37,17 @@ export class TerminalCapabilityManager {
|
|||||||
private static readonly OSC_11_REGEX =
|
private static readonly OSC_11_REGEX =
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
/\x1b\]11;rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(\x1b\\|\x07)?/;
|
/\x1b\]11;rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(\x1b\\|\x07)?/;
|
||||||
|
// modifyOtherKeys response: CSI > 4 ; level m
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
private static readonly MODIFY_OTHER_KEYS_REGEX = /\x1b\[>4;(\d+)m/;
|
||||||
|
|
||||||
private terminalBackgroundColor: TerminalBackgroundColor;
|
private terminalBackgroundColor: TerminalBackgroundColor;
|
||||||
private kittySupported = false;
|
private kittySupported = false;
|
||||||
private kittyEnabled = false;
|
private kittyEnabled = false;
|
||||||
private detectionComplete = false;
|
private detectionComplete = false;
|
||||||
private terminalName: string | undefined;
|
private terminalName: string | undefined;
|
||||||
|
private modifyOtherKeysSupported = false;
|
||||||
|
private modifyOtherKeysEnabled = false;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -78,6 +86,7 @@ export class TerminalCapabilityManager {
|
|||||||
let terminalNameReceived = false;
|
let terminalNameReceived = false;
|
||||||
let deviceAttributesReceived = false;
|
let deviceAttributesReceived = false;
|
||||||
let bgReceived = false;
|
let bgReceived = false;
|
||||||
|
let modifyOtherKeysReceived = false;
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
@@ -96,6 +105,10 @@ export class TerminalCapabilityManager {
|
|||||||
this.enableKittyProtocol();
|
this.enableKittyProtocol();
|
||||||
process.on('exit', () => this.disableKittyProtocol());
|
process.on('exit', () => this.disableKittyProtocol());
|
||||||
process.on('SIGTERM', () => this.disableKittyProtocol());
|
process.on('SIGTERM', () => this.disableKittyProtocol());
|
||||||
|
} else if (this.modifyOtherKeysSupported) {
|
||||||
|
this.enableModifyOtherKeys();
|
||||||
|
process.on('exit', () => this.disableModifyOtherKeys());
|
||||||
|
process.on('SIGTERM', () => this.disableModifyOtherKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@@ -161,6 +174,21 @@ export class TerminalCapabilityManager {
|
|||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for modifyOtherKeys support
|
||||||
|
if (!modifyOtherKeysReceived) {
|
||||||
|
const match = buffer.match(
|
||||||
|
TerminalCapabilityManager.MODIFY_OTHER_KEYS_REGEX,
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
modifyOtherKeysReceived = true;
|
||||||
|
const level = parseInt(match[1], 10);
|
||||||
|
this.modifyOtherKeysSupported = level >= 2;
|
||||||
|
debugLogger.log(
|
||||||
|
`Detected modifyOtherKeys support: ${this.modifyOtherKeysSupported} (level ${level})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.stdin.on('data', onData);
|
process.stdin.on('data', onData);
|
||||||
@@ -171,6 +199,7 @@ export class TerminalCapabilityManager {
|
|||||||
TerminalCapabilityManager.KITTY_QUERY +
|
TerminalCapabilityManager.KITTY_QUERY +
|
||||||
TerminalCapabilityManager.OSC_11_QUERY +
|
TerminalCapabilityManager.OSC_11_QUERY +
|
||||||
TerminalCapabilityManager.TERMINAL_NAME_QUERY +
|
TerminalCapabilityManager.TERMINAL_NAME_QUERY +
|
||||||
|
TerminalCapabilityManager.MODIFY_OTHER_KEYS_QUERY +
|
||||||
TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY,
|
TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -214,6 +243,32 @@ export class TerminalCapabilityManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableModifyOtherKeys(): void {
|
||||||
|
try {
|
||||||
|
if (this.modifyOtherKeysSupported) {
|
||||||
|
enableModifyOtherKeys();
|
||||||
|
this.modifyOtherKeysEnabled = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.warn('Failed to enable modifyOtherKeys protocol:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disableModifyOtherKeys(): void {
|
||||||
|
try {
|
||||||
|
if (this.modifyOtherKeysEnabled) {
|
||||||
|
disableModifyOtherKeys();
|
||||||
|
this.modifyOtherKeysEnabled = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.warn('Failed to disable modifyOtherKeys protocol:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isModifyOtherKeysEnabled(): boolean {
|
||||||
|
return this.modifyOtherKeysEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
private parseColor(rHex: string, gHex: string, bHex: string): string {
|
private parseColor(rHex: string, gHex: string, bHex: string): string {
|
||||||
const parseComponent = (hex: string) => {
|
const parseComponent = (hex: string) => {
|
||||||
const val = parseInt(hex, 16);
|
const val = parseInt(hex, 16);
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export function disableKittyKeyboardProtocol() {
|
|||||||
writeToStdout('\x1b[<u');
|
writeToStdout('\x1b[<u');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enableModifyOtherKeys() {
|
||||||
|
writeToStdout('\x1b[>4;2m');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableModifyOtherKeys() {
|
||||||
|
writeToStdout('\x1b[>4;0m');
|
||||||
|
}
|
||||||
|
|
||||||
export function enableLineWrapping() {
|
export function enableLineWrapping() {
|
||||||
writeToStdout('\x1b[?7h');
|
writeToStdout('\x1b[?7h');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user