mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 00:51:25 -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:
102
packages/cli/src/ui/components/shared/HalfLinePaddedBox.tsx
Normal file
102
packages/cli/src/ui/components/shared/HalfLinePaddedBox.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import {
|
||||
interpolateColor,
|
||||
resolveColor,
|
||||
getSafeLowColorBackground,
|
||||
} from '../../themes/color-utils.js';
|
||||
import { isLowColorDepth } from '../../utils/terminalUtils.js';
|
||||
|
||||
export interface HalfLinePaddedBoxProps {
|
||||
/**
|
||||
* The base color to blend with the terminal background.
|
||||
*/
|
||||
backgroundBaseColor: string;
|
||||
|
||||
/**
|
||||
* The opacity (0-1) for blending the backgroundBaseColor onto the terminal background.
|
||||
*/
|
||||
backgroundOpacity: number;
|
||||
|
||||
/**
|
||||
* Whether to render the solid background color.
|
||||
*/
|
||||
useBackgroundColor?: boolean;
|
||||
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A container component that renders a solid background with half-line padding
|
||||
* at the top and bottom using block characters (▀/▄).
|
||||
*/
|
||||
export const HalfLinePaddedBox: React.FC<HalfLinePaddedBoxProps> = (props) => {
|
||||
if (props.useBackgroundColor === false) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return <HalfLinePaddedBoxInternal {...props} />;
|
||||
};
|
||||
|
||||
const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
backgroundBaseColor,
|
||||
backgroundOpacity,
|
||||
children,
|
||||
}) => {
|
||||
const { terminalWidth, terminalBackgroundColor } = useUIState();
|
||||
const terminalBg = terminalBackgroundColor || 'black';
|
||||
|
||||
const isLowColor = isLowColorDepth();
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
// Interpolated background colors often look bad in 256-color terminals
|
||||
if (isLowColor) {
|
||||
return getSafeLowColorBackground(terminalBg);
|
||||
}
|
||||
|
||||
const resolvedBase =
|
||||
resolveColor(backgroundBaseColor) || backgroundBaseColor;
|
||||
const resolvedTerminalBg = resolveColor(terminalBg) || terminalBg;
|
||||
|
||||
return interpolateColor(
|
||||
resolvedTerminalBg,
|
||||
resolvedBase,
|
||||
backgroundOpacity,
|
||||
);
|
||||
}, [backgroundBaseColor, backgroundOpacity, terminalBg, isLowColor]);
|
||||
|
||||
if (!backgroundColor) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexDirection="column"
|
||||
alignItems="stretch"
|
||||
minHeight={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text backgroundColor={backgroundColor} color={terminalBg}>
|
||||
{'▀'.repeat(terminalWidth)}
|
||||
</Text>
|
||||
</Box>
|
||||
{children}
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text color={terminalBg} backgroundColor={backgroundColor}>
|
||||
{'▄'.repeat(terminalWidth)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -374,4 +374,37 @@ describe('ScrollableList Demo Behavior', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Width Prop', () => {
|
||||
it('should apply the width prop to the container', async () => {
|
||||
const items = [{ id: '1', title: 'Item 1' }];
|
||||
let lastFrame: () => string | undefined;
|
||||
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
<MouseProvider mouseEventsEnabled={false}>
|
||||
<KeypressProvider>
|
||||
<ScrollProvider>
|
||||
<Box width={100} height={20}>
|
||||
<ScrollableList
|
||||
data={items}
|
||||
renderItem={({ item }) => <Text>{item.title}</Text>}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={(item) => item.id}
|
||||
hasFocus={true}
|
||||
width={50}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</KeypressProvider>
|
||||
</MouseProvider>,
|
||||
);
|
||||
lastFrame = result.lastFrame;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Item 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ type VirtualizedListProps<T> = {
|
||||
|
||||
interface ScrollableListProps<T> extends VirtualizedListProps<T> {
|
||||
hasFocus: boolean;
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
export type ScrollableListRef<T> = VirtualizedListRef<T>;
|
||||
@@ -45,7 +46,7 @@ function ScrollableList<T>(
|
||||
props: ScrollableListProps<T>,
|
||||
ref: React.Ref<ScrollableListRef<T>>,
|
||||
) {
|
||||
const { hasFocus } = props;
|
||||
const { hasFocus, width } = props;
|
||||
const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
|
||||
const containerRef = useRef<DOMElement>(null);
|
||||
|
||||
@@ -236,6 +237,7 @@ function ScrollableList<T>(
|
||||
flexGrow={1}
|
||||
flexDirection="column"
|
||||
overflow="hidden"
|
||||
width={width}
|
||||
>
|
||||
<VirtualizedList
|
||||
ref={virtualizedListRef}
|
||||
|
||||
Reference in New Issue
Block a user