mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 07:30:52 -07:00
feat: Detect background color (#15132)
This commit is contained in:
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock dependencies
|
||||
const mocks = vi.hoisted(() => ({
|
||||
writeSync: vi.fn(),
|
||||
enableKittyKeyboardProtocol: vi.fn(),
|
||||
disableKittyKeyboardProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
writeSync: mocks.writeSync,
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
enableKittyKeyboardProtocol: mocks.enableKittyKeyboardProtocol,
|
||||
disableKittyKeyboardProtocol: mocks.disableKittyKeyboardProtocol,
|
||||
}));
|
||||
|
||||
describe('kittyProtocolDetector', () => {
|
||||
let originalStdin: NodeJS.ReadStream & { fd?: number };
|
||||
let originalStdout: NodeJS.WriteStream & { fd?: number };
|
||||
let stdinListeners: Record<string, (data: Buffer) => void> = {};
|
||||
|
||||
// Module functions
|
||||
let detectAndEnableKittyProtocol: typeof import('./kittyProtocolDetector.js').detectAndEnableKittyProtocol;
|
||||
let isKittyProtocolEnabled: typeof import('./kittyProtocolDetector.js').isKittyProtocolEnabled;
|
||||
let enableSupportedProtocol: typeof import('./kittyProtocolDetector.js').enableSupportedProtocol;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
vi.useFakeTimers();
|
||||
|
||||
const mod = await import('./kittyProtocolDetector.js');
|
||||
detectAndEnableKittyProtocol = mod.detectAndEnableKittyProtocol;
|
||||
isKittyProtocolEnabled = mod.isKittyProtocolEnabled;
|
||||
enableSupportedProtocol = mod.enableSupportedProtocol;
|
||||
|
||||
// Mock process.stdin and stdout
|
||||
originalStdin = process.stdin;
|
||||
originalStdout = process.stdout;
|
||||
|
||||
stdinListeners = {};
|
||||
|
||||
Object.defineProperty(process, 'stdin', {
|
||||
value: {
|
||||
isTTY: true,
|
||||
isRaw: false,
|
||||
setRawMode: vi.fn(),
|
||||
on: vi.fn((event, handler) => {
|
||||
stdinListeners[event] = handler;
|
||||
}),
|
||||
removeListener: vi.fn(),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(process, 'stdout', {
|
||||
value: {
|
||||
isTTY: true,
|
||||
fd: 1,
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'stdin', { value: originalStdin });
|
||||
Object.defineProperty(process, 'stdout', { value: originalStdout });
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should resolve immediately if not TTY', async () => {
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: false });
|
||||
await detectAndEnableKittyProtocol();
|
||||
expect(mocks.writeSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enable protocol if response indicates support', async () => {
|
||||
const promise = detectAndEnableKittyProtocol();
|
||||
|
||||
// Simulate response
|
||||
expect(stdinListeners['data']).toBeDefined();
|
||||
|
||||
// Send progressive enhancement response
|
||||
stdinListeners['data'](Buffer.from('\x1b[?u'));
|
||||
|
||||
// Send device attributes response
|
||||
stdinListeners['data'](Buffer.from('\x1b[?c'));
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mocks.enableKittyKeyboardProtocol).toHaveBeenCalled();
|
||||
expect(isKittyProtocolEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not enable protocol if timeout occurs', async () => {
|
||||
const promise = detectAndEnableKittyProtocol();
|
||||
|
||||
// Fast forward time past timeout
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mocks.enableKittyKeyboardProtocol).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should wait longer if progressive enhancement received but not attributes', async () => {
|
||||
const promise = detectAndEnableKittyProtocol();
|
||||
|
||||
// Send progressive enhancement response
|
||||
stdinListeners['data'](Buffer.from('\x1b[?u'));
|
||||
|
||||
// Should not resolve yet
|
||||
vi.advanceTimersByTime(300); // Original timeout passed
|
||||
|
||||
// Send device attributes response late
|
||||
stdinListeners['data'](Buffer.from('\x1b[?c'));
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mocks.enableKittyKeyboardProtocol).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle re-enabling protocol', async () => {
|
||||
// First, simulate successful detection to set kittySupported = true
|
||||
const promise = detectAndEnableKittyProtocol();
|
||||
stdinListeners['data'](Buffer.from('\x1b[?u'));
|
||||
stdinListeners['data'](Buffer.from('\x1b[?c'));
|
||||
await promise;
|
||||
|
||||
// Reset mocks to clear previous calls
|
||||
mocks.enableKittyKeyboardProtocol.mockClear();
|
||||
|
||||
// Now test re-enabling
|
||||
enableSupportedProtocol();
|
||||
expect(mocks.enableKittyKeyboardProtocol).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
let detectionComplete = false;
|
||||
|
||||
let kittySupported = false;
|
||||
|
||||
let kittyEnabled = false;
|
||||
|
||||
/**
|
||||
* Detects Kitty keyboard protocol support.
|
||||
* Definitive document about this protocol lives at https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
* This function should be called once at app startup.
|
||||
*/
|
||||
export async function detectAndEnableKittyProtocol(): Promise<void> {
|
||||
if (detectionComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
detectionComplete = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRawMode = process.stdin.isRaw;
|
||||
if (!originalRawMode) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
let responseBuffer = '';
|
||||
let progressiveEnhancementReceived = false;
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
const finish = () => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = undefined;
|
||||
}
|
||||
process.stdin.removeListener('data', handleData);
|
||||
if (!originalRawMode) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
|
||||
if (kittySupported) {
|
||||
enableSupportedProtocol();
|
||||
process.on('exit', disableAllProtocols);
|
||||
process.on('SIGTERM', disableAllProtocols);
|
||||
}
|
||||
|
||||
detectionComplete = true;
|
||||
resolve();
|
||||
};
|
||||
|
||||
const handleData = (data: Buffer) => {
|
||||
if (timeoutId === undefined) {
|
||||
// Race condition. We have already timed out.
|
||||
return;
|
||||
}
|
||||
responseBuffer += data.toString();
|
||||
|
||||
// Check for progressive enhancement response (CSI ? <flags> u)
|
||||
if (responseBuffer.includes('\x1b[?') && responseBuffer.includes('u')) {
|
||||
progressiveEnhancementReceived = true;
|
||||
// Give more time to get the full set of kitty responses if we have an
|
||||
// indication the terminal probably supports kitty and we just need to
|
||||
// wait a bit longer for a response.
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(finish, 1000);
|
||||
}
|
||||
|
||||
// Check for device attributes response (CSI ? <attrs> c)
|
||||
if (responseBuffer.includes('\x1b[?') && responseBuffer.includes('c')) {
|
||||
if (progressiveEnhancementReceived) {
|
||||
kittySupported = true;
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('data', handleData);
|
||||
|
||||
// Query progressive enhancement and device attributes
|
||||
fs.writeSync(process.stdout.fd, '\x1b[?u\x1b[c');
|
||||
|
||||
// Timeout after 200ms
|
||||
// When a iterm2 terminal does not have focus this can take over 90s on a
|
||||
// fast macbook so we need a somewhat longer threshold than would be ideal.
|
||||
timeoutId = setTimeout(finish, 200);
|
||||
});
|
||||
}
|
||||
|
||||
import {
|
||||
enableKittyKeyboardProtocol,
|
||||
disableKittyKeyboardProtocol,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export function isKittyProtocolEnabled(): boolean {
|
||||
return kittyEnabled;
|
||||
}
|
||||
|
||||
function disableAllProtocols() {
|
||||
try {
|
||||
if (kittyEnabled) {
|
||||
disableKittyKeyboardProtocol();
|
||||
kittyEnabled = false;
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is exported so we can reenable this after exiting an editor which might
|
||||
* change the mode.
|
||||
*/
|
||||
export function enableSupportedProtocol(): void {
|
||||
try {
|
||||
if (kittySupported) {
|
||||
enableKittyKeyboardProtocol();
|
||||
kittyEnabled = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
177
packages/cli/src/ui/utils/terminalCapabilityManager.test.ts
Normal file
177
packages/cli/src/ui/utils/terminalCapabilityManager.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { TerminalCapabilityManager } from './terminalCapabilityManager.js';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
// Mock fs
|
||||
vi.mock('node:fs', () => ({
|
||||
writeSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock core
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger: {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
enableKittyKeyboardProtocol: vi.fn(),
|
||||
disableKittyKeyboardProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TerminalCapabilityManager', () => {
|
||||
let stdin: EventEmitter & {
|
||||
isTTY?: boolean;
|
||||
isRaw?: boolean;
|
||||
setRawMode?: (mode: boolean) => void;
|
||||
removeListener?: (
|
||||
event: string,
|
||||
listener: (...args: unknown[]) => void,
|
||||
) => void;
|
||||
};
|
||||
let stdout: { isTTY?: boolean; fd?: number };
|
||||
// Save original process properties
|
||||
const originalStdin = process.stdin;
|
||||
const originalStdout = process.stdout;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Reset singleton
|
||||
TerminalCapabilityManager.resetInstanceForTesting();
|
||||
|
||||
// Setup process mocks
|
||||
stdin = new EventEmitter();
|
||||
stdin.isTTY = true;
|
||||
stdin.isRaw = false;
|
||||
stdin.setRawMode = vi.fn();
|
||||
stdin.removeListener = vi.fn();
|
||||
|
||||
stdout = { isTTY: true, fd: 1 };
|
||||
|
||||
// Use defineProperty to mock process.stdin/stdout
|
||||
Object.defineProperty(process, 'stdin', {
|
||||
value: stdin,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(process, 'stdout', {
|
||||
value: stdout,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
// Restore original process properties
|
||||
Object.defineProperty(process, 'stdin', {
|
||||
value: originalStdin,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(process, 'stdout', {
|
||||
value: originalStdout,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect Kitty support when u response is received', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Simulate Kitty response: \x1b[?1u
|
||||
stdin.emit('data', Buffer.from('\x1b[?1u'));
|
||||
// Complete detection with DA1
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
await promise;
|
||||
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Background Color', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Simulate OSC 11 response
|
||||
// \x1b]11;rgb:0000/ff00/0000\x1b\
|
||||
// RGB: 0, 255, 0 -> #00ff00
|
||||
stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/ffff/0000\x1b\\'));
|
||||
// Complete detection with DA1
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
await promise;
|
||||
expect(manager.getTerminalBackgroundColor()).toBe('#00ff00');
|
||||
});
|
||||
|
||||
it('should detect Terminal Name', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Simulate Terminal Name response
|
||||
stdin.emit('data', Buffer.from('\x1bP>|WezTerm 20240203\x1b\\'));
|
||||
// Complete detection with DA1
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
await promise;
|
||||
expect(manager.getTerminalName()).toBe('WezTerm 20240203');
|
||||
});
|
||||
|
||||
it('should complete early if sentinel (DA1) is found', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
stdin.emit('data', Buffer.from('\x1b[?1u'));
|
||||
stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/0000/0000\x1b\\'));
|
||||
// Sentinel
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
// Should resolve without waiting for timeout
|
||||
await promise;
|
||||
|
||||
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||
expect(manager.getTerminalBackgroundColor()).toBe('#000000');
|
||||
});
|
||||
|
||||
it('should timeout if no DA1 (c) is received', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Simulate only Kitty response
|
||||
stdin.emit('data', Buffer.from('\x1b[?1u'));
|
||||
|
||||
// Advance to timeout
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await promise;
|
||||
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not detect Kitty if only DA1 (c) is received', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Simulate DA1 response only: \x1b[?62;c
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
await promise;
|
||||
expect(manager.isKittyProtocolEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle split chunks', async () => {
|
||||
const manager = TerminalCapabilityManager.getInstance();
|
||||
const promise = manager.detectCapabilities();
|
||||
|
||||
// Split response: \x1b[? 1u
|
||||
stdin.emit('data', Buffer.from('\x1b[?'));
|
||||
stdin.emit('data', Buffer.from('1u'));
|
||||
// Complete with DA1
|
||||
stdin.emit('data', Buffer.from('\x1b[?62c'));
|
||||
|
||||
await promise;
|
||||
expect(manager.isKittyProtocolEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
237
packages/cli/src/ui/utils/terminalCapabilityManager.ts
Normal file
237
packages/cli/src/ui/utils/terminalCapabilityManager.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import {
|
||||
debugLogger,
|
||||
enableKittyKeyboardProtocol,
|
||||
disableKittyKeyboardProtocol,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export type TerminalBackgroundColor = string | undefined;
|
||||
|
||||
export class TerminalCapabilityManager {
|
||||
private static instance: TerminalCapabilityManager | undefined;
|
||||
|
||||
private static readonly KITTY_QUERY = '\x1b[?u';
|
||||
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';
|
||||
|
||||
// Kitty keyboard flags: CSI ? flags u
|
||||
// eslint-disable-next-line no-control-regex
|
||||
private static readonly KITTY_REGEX = /\x1b\[\?(\d+)u/;
|
||||
// Terminal Name/Version response: DCS > | text ST (or BEL)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
private static readonly TERMINAL_NAME_REGEX = /\x1bP>\|(.+?)(\x1b\\|\x07)/;
|
||||
// Primary Device Attributes: CSI ? ID ; ... c
|
||||
// eslint-disable-next-line no-control-regex
|
||||
private static readonly DEVICE_ATTRIBUTES_REGEX = /\x1b\[\?(\d+)(;\d+)*c/;
|
||||
// OSC 11 response: OSC 11 ; rgb:rrrr/gggg/bbbb ST (or BEL)
|
||||
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)?/;
|
||||
|
||||
private terminalBackgroundColor: TerminalBackgroundColor;
|
||||
private kittySupported = false;
|
||||
private kittyEnabled = false;
|
||||
private detectionComplete = false;
|
||||
private terminalName: string | undefined;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): TerminalCapabilityManager {
|
||||
if (!this.instance) {
|
||||
this.instance = new TerminalCapabilityManager();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
static resetInstanceForTesting(): void {
|
||||
this.instance = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects terminal capabilities (Kitty protocol support, terminal name,
|
||||
* background color).
|
||||
* This should be called once at app startup.
|
||||
*/
|
||||
async detectCapabilities(): Promise<void> {
|
||||
if (this.detectionComplete) return;
|
||||
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
this.detectionComplete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const originalRawMode = process.stdin.isRaw;
|
||||
if (!originalRawMode) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
let buffer = '';
|
||||
let kittyKeyboardReceived = false;
|
||||
let terminalNameReceived = false;
|
||||
let deviceAttributesReceived = false;
|
||||
let bgReceived = false;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
process.stdin.removeListener('data', onData);
|
||||
if (!originalRawMode) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
this.detectionComplete = true;
|
||||
|
||||
// Auto-enable kitty if supported
|
||||
if (this.kittySupported) {
|
||||
this.enableKittyProtocol();
|
||||
process.on('exit', () => this.disableKittyProtocol());
|
||||
process.on('SIGTERM', () => this.disableKittyProtocol());
|
||||
}
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
cleanup();
|
||||
};
|
||||
|
||||
// A somewhat long timeout is acceptable as all terminals should respond
|
||||
// to the device attributes query used as a sentinel.
|
||||
timeoutId = setTimeout(onTimeout, 1000);
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
buffer += data.toString();
|
||||
|
||||
// Check OSC 11
|
||||
if (!bgReceived) {
|
||||
const match = buffer.match(TerminalCapabilityManager.OSC_11_REGEX);
|
||||
if (match) {
|
||||
bgReceived = true;
|
||||
this.terminalBackgroundColor = this.parseColor(
|
||||
match[1],
|
||||
match[2],
|
||||
match[3],
|
||||
);
|
||||
debugLogger.log(
|
||||
`Detected terminal background color: ${this.terminalBackgroundColor}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!kittyKeyboardReceived &&
|
||||
TerminalCapabilityManager.KITTY_REGEX.test(buffer)
|
||||
) {
|
||||
kittyKeyboardReceived = true;
|
||||
this.kittySupported = true;
|
||||
}
|
||||
|
||||
// Check for Terminal Name/Version response.
|
||||
if (!terminalNameReceived) {
|
||||
const match = buffer.match(
|
||||
TerminalCapabilityManager.TERMINAL_NAME_REGEX,
|
||||
);
|
||||
if (match) {
|
||||
terminalNameReceived = true;
|
||||
this.terminalName = match[1];
|
||||
|
||||
debugLogger.log(`Detected terminal name: ${this.terminalName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// We use the Primary Device Attributes response as a sentinel to know
|
||||
// that the terminal has processed all our queries. Since we send it
|
||||
// last, receiving it means we can stop waiting.
|
||||
if (!deviceAttributesReceived) {
|
||||
const match = buffer.match(
|
||||
TerminalCapabilityManager.DEVICE_ATTRIBUTES_REGEX,
|
||||
);
|
||||
if (match) {
|
||||
deviceAttributesReceived = true;
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('data', onData);
|
||||
|
||||
try {
|
||||
fs.writeSync(
|
||||
process.stdout.fd,
|
||||
TerminalCapabilityManager.KITTY_QUERY +
|
||||
TerminalCapabilityManager.OSC_11_QUERY +
|
||||
TerminalCapabilityManager.TERMINAL_NAME_QUERY +
|
||||
TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY,
|
||||
);
|
||||
} catch (e) {
|
||||
debugLogger.warn('Failed to write terminal capability queries:', e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTerminalBackgroundColor(): TerminalBackgroundColor {
|
||||
return this.terminalBackgroundColor;
|
||||
}
|
||||
|
||||
getTerminalName(): string | undefined {
|
||||
return this.terminalName;
|
||||
}
|
||||
|
||||
isKittyProtocolEnabled(): boolean {
|
||||
return this.kittyEnabled;
|
||||
}
|
||||
|
||||
enableKittyProtocol(): void {
|
||||
try {
|
||||
if (this.kittySupported) {
|
||||
enableKittyKeyboardProtocol();
|
||||
this.kittyEnabled = true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.warn('Failed to enable Kitty protocol:', e);
|
||||
}
|
||||
}
|
||||
|
||||
disableKittyProtocol(): void {
|
||||
try {
|
||||
if (this.kittyEnabled) {
|
||||
disableKittyKeyboardProtocol();
|
||||
this.kittyEnabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.warn('Failed to disable Kitty protocol:', e);
|
||||
}
|
||||
}
|
||||
|
||||
private parseColor(rHex: string, gHex: string, bHex: string): string {
|
||||
const parseComponent = (hex: string) => {
|
||||
const val = parseInt(hex, 16);
|
||||
if (hex.length === 1) return (val / 15) * 255;
|
||||
if (hex.length === 2) return val;
|
||||
if (hex.length === 3) return (val / 4095) * 255;
|
||||
if (hex.length === 4) return (val / 65535) * 255;
|
||||
return val;
|
||||
};
|
||||
|
||||
const r = parseComponent(rHex);
|
||||
const g = parseComponent(gHex);
|
||||
const b = parseComponent(bHex);
|
||||
|
||||
const toHex = (c: number) => Math.round(c).toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalCapabilityManager =
|
||||
TerminalCapabilityManager.getInstance();
|
||||
@@ -42,8 +42,10 @@ vi.mock('node:os', () => ({
|
||||
platform: mocks.platform,
|
||||
}));
|
||||
|
||||
vi.mock('./kittyProtocolDetector.js', () => ({
|
||||
isKittyProtocolEnabled: vi.fn().mockReturnValue(false),
|
||||
vi.mock('./terminalCapabilityManager.js', () => ({
|
||||
terminalCapabilityManager: {
|
||||
isKittyProtocolEnabled: vi.fn().mockReturnValue(false),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('terminalSetup', () => {
|
||||
|
||||
@@ -28,7 +28,7 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { isKittyProtocolEnabled } from './kittyProtocolDetector.js';
|
||||
import { terminalCapabilityManager } from './terminalCapabilityManager.js';
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -323,7 +323,7 @@ async function configureWindsurf(): Promise<TerminalSetupResult> {
|
||||
*/
|
||||
export async function terminalSetup(): Promise<TerminalSetupResult> {
|
||||
// Check if terminal already has optimal keyboard support
|
||||
if (isKittyProtocolEnabled()) {
|
||||
if (terminalCapabilityManager.isKittyProtocolEnabled()) {
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
|
||||
Reference in New Issue
Block a user