mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-20 02:51:55 -07:00
feat(cli): implement automatic theme switching based on terminal background (#17976)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
81
packages/cli/src/ui/contexts/TerminalContext.test.tsx
Normal file
81
packages/cli/src/ui/contexts/TerminalContext.test.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { TerminalProvider, useTerminalContext } from './TerminalContext.js';
|
||||
import { vi, describe, it, expect, type Mock } from 'vitest';
|
||||
import { useEffect, act } from 'react';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
const mockStdin = new EventEmitter() as unknown as NodeJS.ReadStream &
|
||||
EventEmitter;
|
||||
// Add required properties for Ink's StdinProps
|
||||
(mockStdin as unknown as { write: Mock }).write = vi.fn();
|
||||
(mockStdin as unknown as { setEncoding: Mock }).setEncoding = vi.fn();
|
||||
(mockStdin as unknown as { setRawMode: Mock }).setRawMode = vi.fn();
|
||||
(mockStdin as unknown as { isTTY: boolean }).isTTY = true;
|
||||
// Mock removeListener specifically as it is used in cleanup
|
||||
(mockStdin as unknown as { removeListener: Mock }).removeListener = vi.fn(
|
||||
(event: string, listener: (...args: unknown[]) => void) => {
|
||||
mockStdin.off(event, listener);
|
||||
},
|
||||
);
|
||||
|
||||
vi.mock('ink', () => ({
|
||||
useStdin: () => ({
|
||||
stdin: mockStdin,
|
||||
}),
|
||||
}));
|
||||
|
||||
const TestComponent = ({ onColor }: { onColor: (c: string) => void }) => {
|
||||
const { subscribe } = useTerminalContext();
|
||||
useEffect(() => {
|
||||
subscribe(onColor);
|
||||
}, [subscribe, onColor]);
|
||||
return null;
|
||||
};
|
||||
|
||||
describe('TerminalContext', () => {
|
||||
it('should parse OSC 11 response', async () => {
|
||||
const handleColor = vi.fn();
|
||||
render(
|
||||
<TerminalProvider>
|
||||
<TestComponent onColor={handleColor} />
|
||||
</TerminalProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
mockStdin.emit('data', '\x1b]11;rgb:ffff/ffff/ffff\x1b\\');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleColor).toHaveBeenCalledWith('rgb:ffff/ffff/ffff');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle partial chunks', async () => {
|
||||
const handleColor = vi.fn();
|
||||
render(
|
||||
<TerminalProvider>
|
||||
<TestComponent onColor={handleColor} />
|
||||
</TerminalProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
mockStdin.emit('data', '\x1b]11;rgb:0000/');
|
||||
});
|
||||
expect(handleColor).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
mockStdin.emit('data', '0000/0000\x1b\\');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleColor).toHaveBeenCalledWith('rgb:0000/0000/0000');
|
||||
});
|
||||
});
|
||||
});
|
||||
96
packages/cli/src/ui/contexts/TerminalContext.tsx
Normal file
96
packages/cli/src/ui/contexts/TerminalContext.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useStdin } from 'ink';
|
||||
import type React from 'react';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { TerminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
|
||||
|
||||
export type TerminalEventHandler = (event: string) => void;
|
||||
|
||||
interface TerminalContextValue {
|
||||
subscribe: (handler: TerminalEventHandler) => void;
|
||||
unsubscribe: (handler: TerminalEventHandler) => void;
|
||||
}
|
||||
|
||||
const TerminalContext = createContext<TerminalContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useTerminalContext() {
|
||||
const context = useContext(TerminalContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useTerminalContext must be used within a TerminalProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function TerminalProvider({ children }: { children: React.ReactNode }) {
|
||||
const { stdin } = useStdin();
|
||||
const subscribers = useRef<Set<TerminalEventHandler>>(new Set()).current;
|
||||
const bufferRef = useRef('');
|
||||
|
||||
const subscribe = useCallback(
|
||||
(handler: TerminalEventHandler) => {
|
||||
subscribers.add(handler);
|
||||
},
|
||||
[subscribers],
|
||||
);
|
||||
|
||||
const unsubscribe = useCallback(
|
||||
(handler: TerminalEventHandler) => {
|
||||
subscribers.delete(handler);
|
||||
},
|
||||
[subscribers],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleData = (data: Buffer | string) => {
|
||||
bufferRef.current +=
|
||||
typeof data === 'string' ? data : data.toString('utf-8');
|
||||
|
||||
// Check for OSC 11 response
|
||||
const match = bufferRef.current.match(
|
||||
TerminalCapabilityManager.OSC_11_REGEX,
|
||||
);
|
||||
if (match) {
|
||||
const colorStr = `rgb:${match[1]}/${match[2]}/${match[3]}`;
|
||||
for (const handler of subscribers) {
|
||||
handler(colorStr);
|
||||
}
|
||||
// Safely remove the processed part + match
|
||||
if (match.index !== undefined) {
|
||||
bufferRef.current = bufferRef.current.slice(
|
||||
match.index + match[0].length,
|
||||
);
|
||||
}
|
||||
} else if (bufferRef.current.length > 4096) {
|
||||
// Safety valve: if buffer gets too large without a match, trim it.
|
||||
// We keep the last 1024 bytes to avoid cutting off a partial sequence.
|
||||
bufferRef.current = bufferRef.current.slice(-1024);
|
||||
}
|
||||
};
|
||||
|
||||
stdin.on('data', handleData);
|
||||
return () => {
|
||||
stdin.removeListener('data', handleData);
|
||||
};
|
||||
}, [stdin, subscribers]);
|
||||
|
||||
return (
|
||||
<TerminalContext.Provider value={{ subscribe, unsubscribe }}>
|
||||
{children}
|
||||
</TerminalContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user