mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -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(),
|
||||
disableKittyKeyboardProtocol: vi.fn(),
|
||||
enableModifyOtherKeys: vi.fn(),
|
||||
disableModifyOtherKeys: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TerminalCapabilityManager', () => {
|
||||
@@ -174,4 +176,92 @@ describe('TerminalCapabilityManager', () => {
|
||||
await promise;
|
||||
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,
|
||||
enableKittyKeyboardProtocol,
|
||||
disableKittyKeyboardProtocol,
|
||||
enableModifyOtherKeys,
|
||||
disableModifyOtherKeys,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export type TerminalBackgroundColor = string | undefined;
|
||||
@@ -20,6 +22,7 @@ export class TerminalCapabilityManager {
|
||||
private static readonly OSC_11_QUERY = '\x1b]11;?\x1b\\';
|
||||
private static readonly TERMINAL_NAME_QUERY = '\x1b[>q';
|
||||
private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c';
|
||||
private static readonly MODIFY_OTHER_KEYS_QUERY = '\x1b[>4;?m';
|
||||
|
||||
// Kitty keyboard flags: CSI ? flags u
|
||||
// eslint-disable-next-line no-control-regex
|
||||
@@ -34,12 +37,17 @@ export class TerminalCapabilityManager {
|
||||
private static readonly OSC_11_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)?/;
|
||||
// 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 kittySupported = false;
|
||||
private kittyEnabled = false;
|
||||
private detectionComplete = false;
|
||||
private terminalName: string | undefined;
|
||||
private modifyOtherKeysSupported = false;
|
||||
private modifyOtherKeysEnabled = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -78,6 +86,7 @@ export class TerminalCapabilityManager {
|
||||
let terminalNameReceived = false;
|
||||
let deviceAttributesReceived = false;
|
||||
let bgReceived = false;
|
||||
let modifyOtherKeysReceived = false;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
@@ -96,6 +105,10 @@ export class TerminalCapabilityManager {
|
||||
this.enableKittyProtocol();
|
||||
process.on('exit', () => this.disableKittyProtocol());
|
||||
process.on('SIGTERM', () => this.disableKittyProtocol());
|
||||
} else if (this.modifyOtherKeysSupported) {
|
||||
this.enableModifyOtherKeys();
|
||||
process.on('exit', () => this.disableModifyOtherKeys());
|
||||
process.on('SIGTERM', () => this.disableModifyOtherKeys());
|
||||
}
|
||||
|
||||
resolve();
|
||||
@@ -161,6 +174,21 @@ export class TerminalCapabilityManager {
|
||||
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);
|
||||
@@ -171,6 +199,7 @@ export class TerminalCapabilityManager {
|
||||
TerminalCapabilityManager.KITTY_QUERY +
|
||||
TerminalCapabilityManager.OSC_11_QUERY +
|
||||
TerminalCapabilityManager.TERMINAL_NAME_QUERY +
|
||||
TerminalCapabilityManager.MODIFY_OTHER_KEYS_QUERY +
|
||||
TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY,
|
||||
);
|
||||
} 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 {
|
||||
const parseComponent = (hex: string) => {
|
||||
const val = parseInt(hex, 16);
|
||||
|
||||
@@ -26,6 +26,14 @@ export function disableKittyKeyboardProtocol() {
|
||||
writeToStdout('\x1b[<u');
|
||||
}
|
||||
|
||||
export function enableModifyOtherKeys() {
|
||||
writeToStdout('\x1b[>4;2m');
|
||||
}
|
||||
|
||||
export function disableModifyOtherKeys() {
|
||||
writeToStdout('\x1b[>4;0m');
|
||||
}
|
||||
|
||||
export function enableLineWrapping() {
|
||||
writeToStdout('\x1b[?7h');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user