fix(cli): improve HalfLinePaddedBox rendering for Apple Terminal

This commit is contained in:
Mark McLaughlin
2026-03-26 10:34:50 -07:00
parent 20fd405f9c
commit 47b0cfd204
8 changed files with 118 additions and 14 deletions
+1
View File
@@ -74,6 +74,7 @@ vi.mock('../ui/utils/terminalUtils.js', () => ({
isLowColorDepth: vi.fn(() => false),
getColorDepth: vi.fn(() => 24),
isITerm2: vi.fn(() => false),
isAppleTerminal: vi.fn(() => false),
}));
type TerminalState = {
@@ -8,7 +8,7 @@ import { renderWithProviders } from '../../../test-utils/render.js';
import { HalfLinePaddedBox } from './HalfLinePaddedBox.js';
import { Text, useIsScreenReaderEnabled } from 'ink';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { isITerm2 } from '../../utils/terminalUtils.js';
import { isITerm2, isAppleTerminal } from '../../utils/terminalUtils.js';
vi.mock('ink', async () => {
const actual = await vi.importActual('ink');
@@ -25,34 +25,55 @@ describe('<HalfLinePaddedBox />', () => {
vi.restoreAllMocks();
});
it('renders standard background and blocks when not iTerm2', async () => {
it('renders standard background and blocks when not iTerm2 or Apple Terminal', async () => {
vi.mocked(isITerm2).mockReturnValue(false);
vi.mocked(isAppleTerminal).mockReturnValue(false);
const { lastFrame, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
expect(lastFrame()).toMatchSnapshot();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
renderResult.unmount();
});
it('renders iTerm2-specific blocks when iTerm2 is detected', async () => {
vi.mocked(isITerm2).mockReturnValue(true);
vi.mocked(isAppleTerminal).mockReturnValue(false);
const { lastFrame, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
expect(lastFrame()).toMatchSnapshot();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
renderResult.unmount();
});
it('renders Apple Terminal-specific blocks when Apple Terminal is detected', async () => {
vi.mocked(isITerm2).mockReturnValue(false);
vi.mocked(isAppleTerminal).mockReturnValue(true);
const renderResult = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
it('renders nothing when useBackgroundColor is false', async () => {
@@ -14,7 +14,11 @@ import {
resolveColor,
getSafeLowColorBackground,
} from '../../themes/color-utils.js';
import { isLowColorDepth, isITerm2 } from '../../utils/terminalUtils.js';
import {
isLowColorDepth,
isITerm2,
isAppleTerminal,
} from '../../utils/terminalUtils.js';
export interface HalfLinePaddedBoxProps {
/**
@@ -108,6 +112,33 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
);
}
const isMacOSTerminal = isAppleTerminal();
if (isMacOSTerminal) {
return (
<Box
width={terminalWidth}
flexDirection="column"
alignItems="stretch"
minHeight={1}
flexShrink={0}
backgroundColor={backgroundColor}
>
<Box width={terminalWidth} flexDirection="row">
<Text backgroundColor={terminalBg} color={backgroundColor}>
{'▅'.repeat(terminalWidth)}
</Text>
</Box>
{children}
<Box width={terminalWidth} flexDirection="row">
<Text color={terminalBg} backgroundColor={backgroundColor}>
{'▄'.repeat(terminalWidth)}
</Text>
</Box>
</Box>
);
}
return (
<Box
width={terminalWidth}
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="110" height="71" viewBox="0 0 110 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="110" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<rect x="0" y="0" width="90" height="17" fill="#000000" />
<text x="0" y="2" fill="#00007f" textLength="90" lengthAdjust="spacingAndGlyphs">▅▅▅▅▅▅▅▅▅▅</text>
<rect x="0" y="17" width="90" height="17" fill="#00007f" />
<text x="0" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Content </text>
<rect x="0" y="34" width="90" height="17" fill="#00007f" />
<text x="0" y="36" fill="#000000" textLength="90" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 882 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="110" height="71" viewBox="0 0 110 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="110" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00007f" textLength="90" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄</text>
<rect x="0" y="17" width="90" height="17" fill="#00007f" />
<text x="0" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Content </text>
<text x="0" y="36" fill="#00007f" textLength="90" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 755 B

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="110" height="71" viewBox="0 0 110 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="110" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<rect x="0" y="0" width="90" height="17" fill="#00007f" />
<text x="0" y="2" fill="#000000" textLength="90" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀</text>
<rect x="0" y="17" width="90" height="17" fill="#00007f" />
<text x="0" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Content </text>
<rect x="0" y="34" width="90" height="17" fill="#00007f" />
<text x="0" y="36" fill="#000000" textLength="90" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 882 B

@@ -1,10 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<HalfLinePaddedBox /> > renders Apple Terminal-specific blocks when Apple Terminal is detected 1`] = `
"▅▅▅▅▅▅▅▅▅▅
Content
▄▄▄▄▄▄▄▄▄▄"
`;
exports[`<HalfLinePaddedBox /> > renders iTerm2-specific blocks when iTerm2 is detected 1`] = `
"▄▄▄▄▄▄▄▄▄▄
Content
▀▀▀▀▀▀▀▀▀▀
"
▀▀▀▀▀▀▀▀▀▀"
`;
exports[`<HalfLinePaddedBox /> > renders nothing when screen reader is enabled 1`] = `
@@ -17,9 +22,8 @@ exports[`<HalfLinePaddedBox /> > renders nothing when useBackgroundColor is fals
"
`;
exports[`<HalfLinePaddedBox /> > renders standard background and blocks when not iTerm2 1`] = `
exports[`<HalfLinePaddedBox /> > renders standard background and blocks when not iTerm2 or Apple Terminal 1`] = `
"▀▀▀▀▀▀▀▀▀▀
Content
▄▄▄▄▄▄▄▄▄▄
"
▄▄▄▄▄▄▄▄▄▄"
`;
@@ -36,6 +36,13 @@ export function isITerm2(): boolean {
return cachedIsITerm2;
}
/**
* Returns true if the current terminal is macOS Terminal.
*/
export function isAppleTerminal(): boolean {
return process.env['TERM_PROGRAM'] === 'Apple_Terminal';
}
/**
* Resets the cached iTerm2 detection value.
* Primarily used for testing.