mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 01:27:41 -07:00
feat(cli): add ui.compact setting and half-sized logos
This commit is contained in:
@@ -243,6 +243,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.compact`** (boolean):
|
||||
- **Description:** Enable a more compact UI layout.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.incrementalRendering`** (boolean):
|
||||
- **Description:** Enable incremental rendering for the UI. This option will
|
||||
reduce flickering but may cause rendering artifacts. Only supported when
|
||||
|
||||
@@ -524,6 +524,15 @@ const SETTINGS_SCHEMA = {
|
||||
'Use an alternate screen buffer for the UI, preserving shell history.',
|
||||
showInDialog: true,
|
||||
},
|
||||
compact: {
|
||||
type: 'boolean',
|
||||
label: 'Compact UI',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable a more compact UI layout.',
|
||||
showInDialog: true,
|
||||
},
|
||||
incrementalRendering: {
|
||||
type: 'boolean',
|
||||
label: 'Incremental Rendering',
|
||||
|
||||
@@ -37,6 +37,27 @@ export const tinyAsciiLogo = `
|
||||
░░░ ░░░░░░░░░
|
||||
`;
|
||||
|
||||
export const shortAsciiLogoCompact = `
|
||||
▟▛▀▀█▖▜█▀▀▜▝██▙▗██▛▝█▛▝██▙ ▜█▘▜█▘
|
||||
▐█ ▐█▄▌ █▌▜█▘█▌ █▌ █▌▜▙▐█ ▐█
|
||||
▝█▖ ▜█▘▐█ ▘▗ █▌ █▌ █▌ █▌ ▜██ ▐█
|
||||
▝▀▀▀▀ ▀▀▀▀▀▝▀▀ ▝▀▀▝▀▀▝▀▀ ▀▀▘▀▀▘
|
||||
`;
|
||||
|
||||
export const longAsciiLogoCompact = `
|
||||
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
|
||||
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
|
||||
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
|
||||
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
|
||||
`;
|
||||
|
||||
export const tinyAsciiLogoCompact = `
|
||||
▝▜▄ ▟▛▀▀█▖
|
||||
▝▜▄▐█
|
||||
▗▟▀ ▝█▖ ▜█▘
|
||||
▝▀ ▝▀▀▀▀
|
||||
`;
|
||||
|
||||
export const shortAsciiLogoIde = `
|
||||
░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
|
||||
░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import {
|
||||
renderWithProviders,
|
||||
createMockSettings,
|
||||
} from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { Header } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
import { longAsciiLogo, longAsciiLogoIde } from './AsciiArt.js';
|
||||
import {
|
||||
longAsciiLogo,
|
||||
longAsciiLogoIde,
|
||||
longAsciiLogoCompact,
|
||||
} from './AsciiArt.js';
|
||||
import * as semanticColors from '../semantic-colors.js';
|
||||
import * as terminalSetup from '../utils/terminalSetup.js';
|
||||
import { Text } from 'ink';
|
||||
@@ -49,7 +56,7 @@ describe('<Header />', () => {
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: longAsciiLogo,
|
||||
@@ -65,7 +72,7 @@ describe('<Header />', () => {
|
||||
});
|
||||
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
|
||||
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: longAsciiLogoIde,
|
||||
@@ -76,7 +83,7 @@ describe('<Header />', () => {
|
||||
|
||||
it('renders custom ASCII art when provided', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
render(
|
||||
renderWithProviders(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
@@ -90,7 +97,7 @@ describe('<Header />', () => {
|
||||
it('renders custom ASCII art as is when running in an IDE', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
|
||||
render(
|
||||
renderWithProviders(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
@@ -102,13 +109,13 @@ describe('<Header />', () => {
|
||||
});
|
||||
|
||||
it('displays the version number when nightly is true', () => {
|
||||
render(<Header version="1.0.0" nightly={true} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={true} />);
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
expect(textCalls[1][0].children.join('')).toBe('v1.0.0');
|
||||
});
|
||||
|
||||
it('does not display the version number when nightly is false', () => {
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: 'v1.0.0',
|
||||
@@ -117,6 +124,45 @@ describe('<Header />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the compact logo when ui.compact is true', () => {
|
||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />, {
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
compact: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: longAsciiLogoCompact,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the version to the right in compact mode when nightly is true', () => {
|
||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<Header version="1.0.0" nightly={true} />,
|
||||
{
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
compact: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toContain('v1.0.0');
|
||||
// In compact mode, logo and version are in the same Box with flexDirection="row"
|
||||
});
|
||||
|
||||
it('renders with no gradient when theme.ui.gradient is undefined', async () => {
|
||||
vi.spyOn(semanticColors, 'theme', 'get').mockReturnValue({
|
||||
text: {
|
||||
@@ -147,10 +193,13 @@ describe('<Header />', () => {
|
||||
},
|
||||
});
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).not.toHaveBeenCalled();
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
expect(textCalls[0][0]).toHaveProperty('color', '#123456');
|
||||
expect(textCalls[textCalls.length - 1][0]).toHaveProperty(
|
||||
'color',
|
||||
'#123456',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders with a single color when theme.ui.gradient has one color', async () => {
|
||||
@@ -159,11 +208,14 @@ describe('<Header />', () => {
|
||||
ui: { gradient: [singleColor] },
|
||||
} as typeof semanticColors.theme);
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).not.toHaveBeenCalled();
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
expect(textCalls.length).toBe(1);
|
||||
expect(textCalls[0][0]).toHaveProperty('color', singleColor);
|
||||
expect(textCalls.length).toBeGreaterThanOrEqual(1);
|
||||
expect(textCalls[textCalls.length - 1][0]).toHaveProperty(
|
||||
'color',
|
||||
singleColor,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders with a gradient when theme.ui.gradient has two or more colors', async () => {
|
||||
@@ -172,7 +224,7 @@ describe('<Header />', () => {
|
||||
ui: { gradient: gradientColors },
|
||||
} as typeof semanticColors.theme);
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
renderWithProviders(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
colors: gradientColors,
|
||||
|
||||
@@ -14,11 +14,15 @@ import {
|
||||
shortAsciiLogoIde,
|
||||
longAsciiLogoIde,
|
||||
tinyAsciiLogoIde,
|
||||
shortAsciiLogoCompact,
|
||||
longAsciiLogoCompact,
|
||||
tinyAsciiLogoCompact,
|
||||
} from './AsciiArt.js';
|
||||
import { getAsciiArtWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { getTerminalProgram } from '../utils/terminalSetup.js';
|
||||
import { useSnowfall } from '../hooks/useSnowfall.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
interface HeaderProps {
|
||||
customAsciiArt?: string; // For user-defined ASCII art
|
||||
@@ -31,25 +35,62 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
version,
|
||||
nightly,
|
||||
}) => {
|
||||
const { merged: settings } = useSettings();
|
||||
const compact = settings.ui?.compact;
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
const isIde = getTerminalProgram();
|
||||
let displayTitle;
|
||||
const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
|
||||
const widthOfShortLogo = getAsciiArtWidth(shortAsciiLogo);
|
||||
|
||||
const longLogo = compact
|
||||
? longAsciiLogoCompact
|
||||
: isIde
|
||||
? longAsciiLogoIde
|
||||
: longAsciiLogo;
|
||||
const shortLogo = compact
|
||||
? shortAsciiLogoCompact
|
||||
: isIde
|
||||
? shortAsciiLogoIde
|
||||
: shortAsciiLogo;
|
||||
const tinyLogo = compact
|
||||
? tinyAsciiLogoCompact
|
||||
: isIde
|
||||
? tinyAsciiLogoIde
|
||||
: tinyAsciiLogo;
|
||||
|
||||
const widthOfLongLogo = getAsciiArtWidth(longLogo);
|
||||
const widthOfShortLogo = getAsciiArtWidth(shortLogo);
|
||||
|
||||
if (customAsciiArt) {
|
||||
displayTitle = customAsciiArt;
|
||||
} else if (terminalWidth >= widthOfLongLogo) {
|
||||
displayTitle = isIde ? longAsciiLogoIde : longAsciiLogo;
|
||||
displayTitle = longLogo;
|
||||
} else if (terminalWidth >= widthOfShortLogo) {
|
||||
displayTitle = isIde ? shortAsciiLogoIde : shortAsciiLogo;
|
||||
displayTitle = shortLogo;
|
||||
} else {
|
||||
displayTitle = isIde ? tinyAsciiLogoIde : tinyAsciiLogo;
|
||||
displayTitle = tinyLogo;
|
||||
}
|
||||
|
||||
const artWidth = getAsciiArtWidth(displayTitle);
|
||||
const title = useSnowfall(displayTitle);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-end"
|
||||
flexDirection="row"
|
||||
flexShrink={0}
|
||||
marginBottom={1}
|
||||
>
|
||||
<ThemedGradient>{title}</ThemedGradient>
|
||||
{nightly && (
|
||||
<Box paddingLeft={2}>
|
||||
<ThemedGradient>v{version}</ThemedGradient>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
|
||||
@@ -302,6 +302,13 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"compact": {
|
||||
"title": "Compact UI",
|
||||
"description": "Enable a more compact UI layout.",
|
||||
"markdownDescription": "Enable a more compact UI layout.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"incrementalRendering": {
|
||||
"title": "Incremental Rendering",
|
||||
"description": "Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled.",
|
||||
|
||||
Reference in New Issue
Block a user