mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-19 18:40:57 -07:00
feat(ui): add solid background color option for input prompt (#16563)
Co-authored-by: Alexander Farber <farber72@outlook.de>
This commit is contained in:
@@ -42,6 +42,8 @@ import stripAnsi from 'strip-ansi';
|
||||
import chalk from 'chalk';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
|
||||
import type { UIState } from '../contexts/UIStateContext.js';
|
||||
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
||||
|
||||
vi.mock('../hooks/useShellHistory.js');
|
||||
vi.mock('../hooks/useCommandCompletion.js');
|
||||
@@ -50,6 +52,9 @@ vi.mock('../hooks/useReverseSearchCompletion.js');
|
||||
vi.mock('clipboardy');
|
||||
vi.mock('../utils/clipboardUtils.js');
|
||||
vi.mock('../hooks/useKittyKeyboardProtocol.js');
|
||||
vi.mock('../utils/terminalUtils.js', () => ({
|
||||
isLowColorDepth: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
const mockSlashCommands: SlashCommand[] = [
|
||||
{
|
||||
@@ -260,6 +265,8 @@ describe('InputPrompt', () => {
|
||||
getProjectRoot: () => path.join('test', 'project'),
|
||||
getTargetDir: () => path.join('test', 'project', 'src'),
|
||||
getVimMode: () => false,
|
||||
getUseBackgroundColor: () => true,
|
||||
getTerminalBackground: () => undefined,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => ['/test/project/src'],
|
||||
}),
|
||||
@@ -1320,6 +1327,168 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('Background Color Styles', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(isLowColorDepth).mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render with background color by default', async () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain('▀');
|
||||
expect(frame).toContain('▄');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ color: 'black', name: 'black' },
|
||||
{ color: '#000000', name: '#000000' },
|
||||
{ color: '#000', name: '#000' },
|
||||
{ color: undefined, name: 'default (black)' },
|
||||
{ color: 'white', name: 'white' },
|
||||
{ color: '#ffffff', name: '#ffffff' },
|
||||
{ color: '#fff', name: '#fff' },
|
||||
])(
|
||||
'should render with safe grey background but NO side borders in 8-bit mode when background is $name',
|
||||
async ({ color }) => {
|
||||
vi.mocked(isLowColorDepth).mockReturnValue(true);
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiState: {
|
||||
terminalBackgroundColor: color,
|
||||
} as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
|
||||
const isWhite =
|
||||
color === 'white' || color === '#ffffff' || color === '#fff';
|
||||
const expectedBgColor = isWhite ? '#eeeeee' : '#1c1c1c';
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
|
||||
// Use chalk to get the expected background color escape sequence
|
||||
const bgCheck = chalk.bgHex(expectedBgColor)(' ');
|
||||
const bgCode = bgCheck.substring(0, bgCheck.indexOf(' '));
|
||||
|
||||
// Background color code should be present
|
||||
expect(frame).toContain(bgCode);
|
||||
// Background characters should be rendered
|
||||
expect(frame).toContain('▀');
|
||||
expect(frame).toContain('▄');
|
||||
// Side borders should STILL be removed
|
||||
expect(frame).not.toContain('│');
|
||||
});
|
||||
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it('should NOT render with background color but SHOULD render horizontal lines when color depth is < 24 and background is NOT black', async () => {
|
||||
vi.mocked(isLowColorDepth).mockReturnValue(true);
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiState: {
|
||||
terminalBackgroundColor: '#333333',
|
||||
} as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).not.toContain('▀');
|
||||
expect(frame).not.toContain('▄');
|
||||
// It SHOULD have horizontal fallback lines
|
||||
expect(frame).toContain('─');
|
||||
// It SHOULD NOT have vertical side borders (standard Box borders have │)
|
||||
expect(frame).not.toContain('│');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
it('should handle 4-bit color mode (16 colors) as low color depth', async () => {
|
||||
vi.mocked(isLowColorDepth).mockReturnValue(true);
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
|
||||
expect(frame).toContain('▀');
|
||||
|
||||
expect(frame).not.toContain('│');
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render horizontal lines (but NO background) in 8-bit mode when background is blue', async () => {
|
||||
vi.mocked(isLowColorDepth).mockReturnValue(true);
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
|
||||
{
|
||||
uiState: {
|
||||
terminalBackgroundColor: 'blue',
|
||||
} as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
|
||||
// Should NOT have background characters
|
||||
|
||||
expect(frame).not.toContain('▀');
|
||||
|
||||
expect(frame).not.toContain('▄');
|
||||
|
||||
// Should HAVE horizontal lines from the fallback Box borders
|
||||
|
||||
// Box style "round" uses these for top/bottom
|
||||
|
||||
expect(frame).toContain('─');
|
||||
|
||||
// Should NOT have vertical side borders
|
||||
|
||||
expect(frame).not.toContain('│');
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render with plain borders when useBackgroundColor is false', async () => {
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).not.toContain('▀');
|
||||
expect(frame).not.toContain('▄');
|
||||
// Check for Box borders (round style uses unicode box chars)
|
||||
expect(frame).toMatch(/[─│┐└┘┌]/);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursor-based completion trigger', () => {
|
||||
it.each([
|
||||
{
|
||||
@@ -1564,11 +1733,11 @@ describe('InputPrompt', () => {
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualCursor = visualCursor as [number, number];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
@@ -1621,11 +1790,11 @@ describe('InputPrompt', () => {
|
||||
mockBuffer.visualToLogicalMap = visualToLogicalMap as Array<
|
||||
[number, number]
|
||||
>;
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
@@ -1645,11 +1814,11 @@ describe('InputPrompt', () => {
|
||||
[1, 0],
|
||||
[2, 0],
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const lines = frame!.split('\n');
|
||||
@@ -1673,15 +1842,15 @@ describe('InputPrompt', () => {
|
||||
mockBuffer.visualCursor = [2, 5]; // cursor at the end of "world"
|
||||
// Provide a visual-to-logical mapping for each visual line
|
||||
mockBuffer.visualToLogicalMap = [
|
||||
[0, 0], // 'hello' starts at col 0 of logical line 0
|
||||
[1, 0], // '' (blank) is logical line 1, col 0
|
||||
[2, 0], // 'world' is logical line 2, col 0
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[2, 0],
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
// Check that all lines, including the empty one, are rendered.
|
||||
@@ -2505,20 +2674,23 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-collapsed-match',
|
||||
);
|
||||
expect(stdout.lastFrame()).toContain('(r:)');
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-collapsed-match',
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[C');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-expanded-match',
|
||||
);
|
||||
// Just wait for any update to ensure it is stable.
|
||||
// We could also wait for specific text if we knew it.
|
||||
expect(stdout.lastFrame()).toContain('(r:)');
|
||||
});
|
||||
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-expanded-match',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2637,28 +2809,28 @@ describe('InputPrompt', () => {
|
||||
name: 'first line, first char',
|
||||
relX: 0,
|
||||
relY: 0,
|
||||
mouseCol: 5,
|
||||
mouseCol: 4,
|
||||
mouseRow: 2,
|
||||
},
|
||||
{
|
||||
name: 'first line, middle char',
|
||||
relX: 6,
|
||||
relY: 0,
|
||||
mouseCol: 11,
|
||||
mouseCol: 10,
|
||||
mouseRow: 2,
|
||||
},
|
||||
{
|
||||
name: 'second line, first char',
|
||||
relX: 0,
|
||||
relY: 1,
|
||||
mouseCol: 5,
|
||||
mouseCol: 4,
|
||||
mouseRow: 3,
|
||||
},
|
||||
{
|
||||
name: 'second line, end char',
|
||||
relX: 5,
|
||||
relY: 1,
|
||||
mouseCol: 10,
|
||||
mouseCol: 9,
|
||||
mouseRow: 3,
|
||||
},
|
||||
])(
|
||||
@@ -2685,7 +2857,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// Simulate left mouse press at calculated coordinates.
|
||||
// Assumes inner box is at x=4, y=1 based on border(1)+padding(1)+prompt(2) and border-top(1).
|
||||
// Without left border: inner box is at x=3, y=1 based on padding(1)+prompt(2) and border-top(1).
|
||||
await act(async () => {
|
||||
stdin.write(`\x1b[<0;${mouseCol};${mouseRow}M`);
|
||||
});
|
||||
@@ -2727,6 +2899,37 @@ describe('InputPrompt', () => {
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should move cursor on mouse click with plain borders', async () => {
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
props.buffer.text = 'hello world';
|
||||
props.buffer.lines = ['hello world'];
|
||||
props.buffer.viewportVisualLines = ['hello world'];
|
||||
props.buffer.visualToLogicalMap = [[0, 0]];
|
||||
props.buffer.visualCursor = [0, 11];
|
||||
props.buffer.visualScrollRow = 0;
|
||||
|
||||
const { stdin, stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{ mouseEventsEnabled: true, uiActions },
|
||||
);
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('hello world');
|
||||
});
|
||||
|
||||
// With plain borders: 1(border) + 1(padding) + 2(prompt) = 4 offset (x=4, col=5)
|
||||
await act(async () => {
|
||||
stdin.write(`\x1b[<0;5;2M`); // Click at col 5, row 2
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.moveToVisualPosition).toHaveBeenCalledWith(0, 0);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('queued message editing', () => {
|
||||
@@ -2889,7 +3092,8 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toContain('!'));
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2898,7 +3102,8 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toContain('>'));
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2907,10 +3112,10 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toContain('*'));
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not show inverted cursor when shell is focused', async () => {
|
||||
props.isEmbeddedShellFocused = true;
|
||||
props.focus = false;
|
||||
@@ -2919,8 +3124,8 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -3022,8 +3227,9 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
expect(stdout.lastFrame()).toContain('[Image');
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -3040,8 +3246,9 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
expect(stdout.lastFrame()).toContain('@/path/to/screenshots');
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user