Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ccstatusline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions src/tui/components/GlobalOverridesMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ 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
Expand Down Expand Up @@ -133,6 +134,15 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ 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;
Expand Down Expand Up @@ -222,6 +232,12 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
<Text dimColor> - Press (o) to toggle</Text>
</Box>

<Box>
<Text>Minimalist Mode: </Text>
<Text color={minimalistMode ? 'green' : 'red'}>{minimalistMode ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (m) to toggle</Text>
</Box>

<Box>
<Text> Default Padding: </Text>
<Text color='cyan'>{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
Expand Down Expand Up @@ -304,6 +320,9 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
<Text dimColor wrap='wrap'>
• Global Bold: Makes all text bold regardless of individual settings
</Text>
<Text dimColor wrap='wrap'>
• Minimalist Mode: Strips decorative prefixes and labels from widgets
</Text>
<Text dimColor wrap='wrap'>
• Override colors: All widgets will use these colors instead of their configured colors
</Text>
Expand Down
3 changes: 2 additions & 1 deletion src/tui/components/StatusLinePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const renderSingleLine = (
const context: RenderContext = {
terminalWidth,
isPreview: true,
minimalist: settings.minimalistMode ?? false,
lineIndex,
globalSeparatorIndex
};
Expand All @@ -52,7 +53,7 @@ export const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ 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;
Expand Down
186 changes: 186 additions & 0 deletions src/tui/components/__tests__/GlobalOverridesMenu.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
1 change: 1 addition & 0 deletions src/types/RenderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions src/types/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
31 changes: 31 additions & 0 deletions src/utils/__tests__/renderer-ansi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
3 changes: 2 additions & 1 deletion src/utils/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down