diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index a9e997a859..4425e12568 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -42,6 +42,7 @@ vi.mock('../ui/utils/terminalUtils.js', () => ({ isLowColorDepth: vi.fn(() => false), getColorDepth: vi.fn(() => 24), isITerm2: vi.fn(() => false), + shouldUseEmoji: vi.fn(() => true), })); // Wrapper around ink-testing-library's render that ensures act() is called diff --git a/packages/cli/src/ui/components/shared/IconText.tsx b/packages/cli/src/ui/components/shared/IconText.tsx index f23bb6a72d..1b7ecbde01 100644 --- a/packages/cli/src/ui/components/shared/IconText.tsx +++ b/packages/cli/src/ui/components/shared/IconText.tsx @@ -6,7 +6,7 @@ import type React from 'react'; import { Text } from 'ink'; -import process from 'node:process'; +import { shouldUseEmoji } from '../../utils/terminalUtils.js'; interface IconTextProps { icon: string; @@ -30,22 +30,3 @@ export const IconText: React.FC = ({ ); }; - -function shouldUseEmoji(): boolean { - const locale = ( - process.env['LC_ALL'] || - process.env['LC_CTYPE'] || - process.env['LANG'] || - '' - ).toLowerCase(); - const supportsUtf8 = locale.includes('utf-8') || locale.includes('utf8'); - if (!supportsUtf8) { - return false; - } - - if (process.env['TERM'] === 'linux') { - return false; - } - - return true; -} diff --git a/packages/cli/src/ui/utils/terminalUtils.test.ts b/packages/cli/src/ui/utils/terminalUtils.test.ts index 70b2a08f17..7cc3b23ad1 100644 --- a/packages/cli/src/ui/utils/terminalUtils.test.ts +++ b/packages/cli/src/ui/utils/terminalUtils.test.ts @@ -5,12 +5,16 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { isITerm2, resetITerm2Cache } from './terminalUtils.js'; +import { isITerm2, resetITerm2Cache, shouldUseEmoji } from './terminalUtils.js'; describe('terminalUtils', () => { beforeEach(() => { vi.stubEnv('TERM_PROGRAM', ''); vi.stubEnv('ITERM_SESSION_ID', ''); + vi.stubEnv('LC_ALL', ''); + vi.stubEnv('LC_CTYPE', ''); + vi.stubEnv('LANG', ''); + vi.stubEnv('TERM', ''); resetITerm2Cache(); }); @@ -19,30 +23,61 @@ describe('terminalUtils', () => { vi.restoreAllMocks(); }); - it('should detect iTerm2 via TERM_PROGRAM', () => { - vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); - expect(isITerm2()).toBe(true); + describe('isITerm2', () => { + it('should detect iTerm2 via TERM_PROGRAM', () => { + vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); + expect(isITerm2()).toBe(true); + }); + + it('should detect iTerm2 via ITERM_SESSION_ID', () => { + vi.stubEnv('ITERM_SESSION_ID', 'w0t0p0:6789...'); + expect(isITerm2()).toBe(true); + }); + + it('should return false if not iTerm2', () => { + vi.stubEnv('TERM_PROGRAM', 'vscode'); + expect(isITerm2()).toBe(false); + }); + + it('should cache the result', () => { + vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); + expect(isITerm2()).toBe(true); + + // Change env but should still be true due to cache + vi.stubEnv('TERM_PROGRAM', 'vscode'); + expect(isITerm2()).toBe(true); + + resetITerm2Cache(); + expect(isITerm2()).toBe(false); + }); }); - it('should detect iTerm2 via ITERM_SESSION_ID', () => { - vi.stubEnv('ITERM_SESSION_ID', 'w0t0p0:6789...'); - expect(isITerm2()).toBe(true); - }); + describe('shouldUseEmoji', () => { + it('should return true when UTF-8 is supported', () => { + vi.stubEnv('LANG', 'en_US.UTF-8'); + expect(shouldUseEmoji()).toBe(true); + }); - it('should return false if not iTerm2', () => { - vi.stubEnv('TERM_PROGRAM', 'vscode'); - expect(isITerm2()).toBe(false); - }); + it('should return true when utf8 (no hyphen) is supported', () => { + vi.stubEnv('LANG', 'en_US.utf8'); + expect(shouldUseEmoji()).toBe(true); + }); - it('should cache the result', () => { - vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); - expect(isITerm2()).toBe(true); + it('should check LC_ALL first', () => { + vi.stubEnv('LC_ALL', 'en_US.UTF-8'); + vi.stubEnv('LANG', 'C'); + expect(shouldUseEmoji()).toBe(true); + }); - // Change env but should still be true due to cache - vi.stubEnv('TERM_PROGRAM', 'vscode'); - expect(isITerm2()).toBe(true); + it('should return false when UTF-8 is not supported', () => { + vi.stubEnv('LANG', 'C'); + expect(shouldUseEmoji()).toBe(false); + }); - resetITerm2Cache(); - expect(isITerm2()).toBe(false); + it('should return false on linux console (TERM=linux)', () => { + vi.stubEnv('LANG', 'en_US.UTF-8'); + vi.stubEnv('TERM', 'linux'); + expect(shouldUseEmoji()).toBe(false); + }); }); }); diff --git a/packages/cli/src/ui/utils/terminalUtils.ts b/packages/cli/src/ui/utils/terminalUtils.ts index 5c03198f71..ea83c43e89 100644 --- a/packages/cli/src/ui/utils/terminalUtils.ts +++ b/packages/cli/src/ui/utils/terminalUtils.ts @@ -45,3 +45,25 @@ export function isITerm2(): boolean { export function resetITerm2Cache(): void { cachedIsITerm2 = undefined; } + +/** + * Returns true if the terminal likely supports emoji. + */ +export function shouldUseEmoji(): boolean { + const locale = ( + process.env['LC_ALL'] || + process.env['LC_CTYPE'] || + process.env['LANG'] || + '' + ).toLowerCase(); + const supportsUtf8 = locale.includes('utf-8') || locale.includes('utf8'); + if (!supportsUtf8) { + return false; + } + + if (process.env['TERM'] === 'linux') { + return false; + } + + return true; +}