diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 6fe1c22..86c5b37 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -149,7 +149,8 @@ async function renderMultipleLines(data: StatusJSON) { usageData, sessionDuration, skillsMetrics, - isPreview: false + isPreview: false, + minimalist: settings.minimalistMode ?? false }; // Always pre-render all widgets once (for efficiency) diff --git a/src/tui/components/GlobalOverridesMenu.tsx b/src/tui/components/GlobalOverridesMenu.tsx index ebc3a04..f0dc94b 100644 --- a/src/tui/components/GlobalOverridesMenu.tsx +++ b/src/tui/components/GlobalOverridesMenu.tsx @@ -29,6 +29,7 @@ export const GlobalOverridesMenu: React.FC = ({ settin const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator ?? ''); const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors); const [globalBold, setGlobalBold] = useState(settings.globalBold); + const [minimalistMode, setMinimalistMode] = useState(settings.minimalistMode); const isPowerlineEnabled = settings.powerline.enabled; // Check if there are any manual separators in the current configuration @@ -133,6 +134,15 @@ export const GlobalOverridesMenu: React.FC = ({ settin globalBold: newGlobalBold }; onUpdate(updatedSettings); + } else if (input === 'm' || input === 'M') { + // Toggle minimalist mode + const newMinimalistMode = !minimalistMode; + setMinimalistMode(newMinimalistMode); + const updatedSettings = { + ...settings, + minimalistMode: newMinimalistMode + }; + onUpdate(updatedSettings); } else if (input === 'f' || input === 'F') { // Cycle through foreground colors const nextIndex = (currentFgIndex + 1) % fgColors.length; @@ -222,6 +232,12 @@ export const GlobalOverridesMenu: React.FC = ({ settin - Press (o) to toggle + + Minimalist Mode: + {minimalistMode ? '✓ Enabled' : '✗ Disabled'} + - Press (m) to toggle + + Default Padding: {settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'} @@ -304,6 +320,9 @@ export const GlobalOverridesMenu: React.FC = ({ settin • Global Bold: Makes all text bold regardless of individual settings + + • Minimalist Mode: Strips decorative prefixes and labels from widgets + • Override colors: All widgets will use these colors instead of their configured colors diff --git a/src/tui/components/StatusLinePreview.tsx b/src/tui/components/StatusLinePreview.tsx index 9db79da..2e595e9 100644 --- a/src/tui/components/StatusLinePreview.tsx +++ b/src/tui/components/StatusLinePreview.tsx @@ -37,6 +37,7 @@ const renderSingleLine = ( const context: RenderContext = { terminalWidth, isPreview: true, + minimalist: settings.minimalistMode ?? false, lineIndex, globalSeparatorIndex }; @@ -52,7 +53,7 @@ export const StatusLinePreview: React.FC = ({ lines, ter return { renderedLines: [], anyTruncated: false }; // Always pre-render all widgets once (for efficiency) - const preRenderedLines = preRenderAllWidgets(lines, settings, { terminalWidth, isPreview: true }); + const preRenderedLines = preRenderAllWidgets(lines, settings, { terminalWidth, isPreview: true, minimalist: settings.minimalistMode ?? false }); const preCalculatedMaxWidths = calculateMaxWidthsFromPreRendered(preRenderedLines, settings); let globalSeparatorIndex = 0; diff --git a/src/tui/components/__tests__/GlobalOverridesMenu.test.ts b/src/tui/components/__tests__/GlobalOverridesMenu.test.ts new file mode 100644 index 0000000..f822f9a --- /dev/null +++ b/src/tui/components/__tests__/GlobalOverridesMenu.test.ts @@ -0,0 +1,186 @@ +import { render } from 'ink'; +import { PassThrough } from 'node:stream'; +import React from 'react'; +import { + afterEach, + describe, + expect, + it, + vi +} from 'vitest'; + +import { DEFAULT_SETTINGS } from '../../../types/Settings'; +import { GlobalOverridesMenu } from '../GlobalOverridesMenu'; + +class MockTtyStream extends PassThrough { + isTTY = true; + columns = 120; + rows = 40; + + setRawMode() { + return this; + } + + ref() { + return this; + } + + unref() { + return this; + } +} + +interface CapturedWriteStream extends NodeJS.WriteStream { + clearOutput: () => void; + getOutput: () => string; +} + +function createMockStdin(): NodeJS.ReadStream { + return new MockTtyStream() as unknown as NodeJS.ReadStream; +} + +function createMockStdout(): CapturedWriteStream { + const stream = new MockTtyStream(); + const chunks: string[] = []; + + stream.on('data', (chunk: Buffer | string) => { + chunks.push(chunk.toString()); + }); + + return Object.assign(stream as unknown as NodeJS.WriteStream, { + clearOutput() { + chunks.length = 0; + }, + getOutput() { + return chunks.join(''); + } + }); +} + +function flushInk() { + return new Promise((resolve) => { + setTimeout(resolve, 25); + }); +} + +describe('GlobalOverridesMenu', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('displays minimalist mode as disabled by default', async () => { + const stdin = createMockStdin(); + const stdout = createMockStdout(); + const stderr = createMockStdout(); + const onUpdate = vi.fn(); + const onBack = vi.fn(); + + const instance = render( + React.createElement(GlobalOverridesMenu, { + settings: DEFAULT_SETTINGS, + onUpdate, + onBack + }), + { + stdin, + stdout, + stderr, + debug: true, + exitOnCtrlC: false, + patchConsole: false + } + ); + + try { + await flushInk(); + expect(stdout.getOutput()).toContain('Minimalist Mode:'); + expect(stdout.getOutput()).toContain('✗ Disabled'); + } finally { + instance.unmount(); + instance.cleanup(); + stdin.destroy(); + stdout.destroy(); + stderr.destroy(); + } + }); + + it('toggles minimalist mode on when (m) is pressed', async () => { + const stdin = createMockStdin(); + const stdout = createMockStdout(); + const stderr = createMockStdout(); + const onUpdate = vi.fn(); + const onBack = vi.fn(); + + const instance = render( + React.createElement(GlobalOverridesMenu, { + settings: { ...DEFAULT_SETTINGS, minimalistMode: false }, + onUpdate, + onBack + }), + { + stdin, + stdout, + stderr, + debug: true, + exitOnCtrlC: false, + patchConsole: false + } + ); + + try { + await flushInk(); + stdin.write('m'); + await flushInk(); + + expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ + minimalistMode: true + })); + } finally { + instance.unmount(); + instance.cleanup(); + stdin.destroy(); + stdout.destroy(); + stderr.destroy(); + } + }); + + it('toggles minimalist mode off when (m) is pressed while enabled', async () => { + const stdin = createMockStdin(); + const stdout = createMockStdout(); + const stderr = createMockStdout(); + const onUpdate = vi.fn(); + const onBack = vi.fn(); + + const instance = render( + React.createElement(GlobalOverridesMenu, { + settings: { ...DEFAULT_SETTINGS, minimalistMode: true }, + onUpdate, + onBack + }), + { + stdin, + stdout, + stderr, + debug: true, + exitOnCtrlC: false, + patchConsole: false + } + ); + + try { + await flushInk(); + stdin.write('m'); + await flushInk(); + + expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ + minimalistMode: false + })); + } finally { + instance.unmount(); + instance.cleanup(); + stdin.destroy(); + stdout.destroy(); + stderr.destroy(); + } + }); +}); diff --git a/src/types/RenderContext.ts b/src/types/RenderContext.ts index 9ba86ae..6cc69c1 100644 --- a/src/types/RenderContext.ts +++ b/src/types/RenderContext.ts @@ -30,6 +30,7 @@ export interface RenderContext { skillsMetrics?: SkillsMetrics | null; terminalWidth?: number | null; isPreview?: boolean; + minimalist?: boolean; lineIndex?: number; // Index of the current line being rendered (for theme cycling) globalSeparatorIndex?: number; // Global separator index that continues across lines } \ No newline at end of file diff --git a/src/types/Settings.ts b/src/types/Settings.ts index ebde1fd..817dd73 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -49,6 +49,7 @@ export const SettingsSchema = z.object({ overrideBackgroundColor: z.string().optional(), overrideForegroundColor: z.string().optional(), globalBold: z.boolean().default(false), + minimalistMode: z.boolean().default(false), powerline: PowerlineConfigSchema.default({ enabled: false, separators: ['\uE0B0'], diff --git a/src/utils/__tests__/renderer-ansi.test.ts b/src/utils/__tests__/renderer-ansi.test.ts index ad0d1fc..45db272 100644 --- a/src/utils/__tests__/renderer-ansi.test.ts +++ b/src/utils/__tests__/renderer-ansi.test.ts @@ -190,4 +190,35 @@ describe('renderer ANSI/OSC handling', () => { expect(truncated).toContain(OSC8_CLOSE); expect(getVisibleWidth(truncated)).toBeLessThanOrEqual(8); }); +}); + +describe('renderer minimalist mode', () => { + it('renders widget as raw value when minimalist mode is enabled', () => { + const widgets: WidgetItem[] = [{ id: 'model1', type: 'model' }]; + const settings = createSettings(); + const context: RenderContext = { + isPreview: true, + minimalist: true + }; + + const preRenderedLines = preRenderAllWidgets([widgets], settings, context); + const content = preRenderedLines[0]?.[0]?.content; + + // With minimalist mode, model widget should render raw value ('Claude') not 'Model: Claude' + expect(content).toBe('Claude'); + }); + + it('renders widget with label when minimalist mode is disabled', () => { + const widgets: WidgetItem[] = [{ id: 'model1', type: 'model' }]; + const settings = createSettings(); + const context: RenderContext = { + isPreview: true, + minimalist: false + }; + + const preRenderedLines = preRenderAllWidgets([widgets], settings, context); + const content = preRenderedLines[0]?.[0]?.content; + + expect(content).toBe('Model: Claude'); + }); }); \ No newline at end of file diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 546f30d..5f7d7ac 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -513,7 +513,8 @@ export function preRenderAllWidgets( continue; } - const widgetText = widgetImpl.render(widget, context, settings) ?? ''; + const effectiveWidget = context.minimalist ? { ...widget, rawValue: true } : widget; + const widgetText = widgetImpl.render(effectiveWidget, context, settings) ?? ''; // Store the rendered content without padding (padding is applied later) // Use stringWidth to properly calculate Unicode character display width