Couple bug fixes relating to colors

This commit is contained in:
Matthew Breedlove
2025-08-11 18:10:33 -04:00
parent 7b4c775c8d
commit 4465ac8053
5 changed files with 99 additions and 103 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ Configure global formatting preferences that apply to all status items:
> 💡 **Note:** These settings are applied during rendering and don't add items to your widget list. They provide a consistent look across your entire status line without modifying individual item configurations.
> ⚠️ **VSCode Users:** If colors appear incorrect in the VSCode integrated terminal, your VSCode theme may be overriding the ANSI color codes. This is a known limitation of VSCode's terminal rendering. The status line will display correctly in standalone terminals.
> ⚠️ **VSCode Users:** If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.
### 🔤 Raw Value Mode
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "1.1.1",
"version": "1.1.2",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
+63 -40
View File
@@ -15,7 +15,7 @@ chalk.level = 3;
// Helper function to apply foreground, background colors and bold
function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string {
let result = text;
// Standard color application
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
@@ -24,18 +24,18 @@ function applyColors(text: string, foregroundColor?: string, backgroundColor?: s
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
}
@@ -98,6 +98,29 @@ async function readStdin(): Promise<string | null> {
}
}
// Helper to get default color for an item type
function getItemDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
@@ -334,20 +357,20 @@ async function getTokenMetrics(transcriptPath: string): Promise<{
}
function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, tokenMetrics: any, sessionDuration: string | null): string {
// Helper to apply colors with optional background and bold override
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
// Override foreground color takes precedence over EVERYTHING, including passed foreground color
let fgColor = foregroundColor;
if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING, including passed background color
let bgColor = backgroundColor;
if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings.globalBold || bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
@@ -394,9 +417,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
case 'model':
if (data.model) {
const text = item.rawValue ? data.model.display_name : `Model: ${data.model.display_name}`;
// Use item.color first, then fall back to settings.colors.model
const defaultColor = item.color || settings.colors.model;
elements.push({ content: applyColorsWithOverride(text, defaultColor, item.backgroundColor, item.bold), type: 'model', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
}
break;
@@ -404,7 +425,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
const branch = getGitBranch();
if (branch) {
const text = item.rawValue ? branch : `${branch}`;
elements.push({ content: applyColorsWithOverride(text, item.color || settings.colors.gitBranch, item.backgroundColor, item.bold), type: 'git-branch', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
}
break;
@@ -420,35 +441,35 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
case 'tokens-input':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.inputTokens) : `In: ${formatTokens(tokenMetrics.inputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'tokens-input', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.outputTokens) : `Out: ${formatTokens(tokenMetrics.outputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'tokens-output', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.cachedTokens) : `Cached: ${formatTokens(tokenMetrics.cachedTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-cached', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.totalTokens) : `Total: ${formatTokens(tokenMetrics.totalTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-total', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.contextLength) : `Ctx: ${formatTokens(tokenMetrics.contextLength)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-length', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
@@ -456,7 +477,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-percentage', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
@@ -465,14 +486,14 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (tokenMetrics.contextLength / 160000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const detectedWidth = terminalWidth || getTerminalWidth();
if (detectedWidth) {
const text = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
const width = terminalWidth || getTerminalWidth();
if (width) {
const text = item.rawValue ? `${width}` : `Term: ${width}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
@@ -480,7 +501,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
case 'session-clock':
if (sessionDuration) {
const text = item.rawValue ? sessionDuration : `Session: ${sessionDuration}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'session-clock', item });
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
@@ -504,8 +525,8 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
} else {
sepText = ` ${sepChar} `;
}
// Use item color if specified, otherwise use settings color or default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || settings.colors.separator || 'gray', item.backgroundColor, item.bold);
// Use item color if specified, otherwise default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold);
elements.push({ content: sepContent, type: 'separator', item });
}
break;
@@ -531,10 +552,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
stdio: ['pipe', 'pipe', 'ignore'],
timeout: timeout
}).trim();
if (output) {
let finalOutput = output;
// Handle max width truncation
if (item.maxWidth && item.maxWidth > 0) {
// Remove ANSI codes to measure actual length
@@ -545,7 +566,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
for (let i = 0; i < output.length; i++) {
const char = output[i];
if (char === '\x1b') {
@@ -570,7 +591,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
finalOutput = truncated;
}
}
// Apply color if not preserving original colors
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
@@ -595,12 +616,12 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') {
elements.pop();
}
// Apply default padding and separators
const finalElements: string[] = [];
const padding = settings.defaultPadding || '';
const defaultSep = settings.defaultSeparator || '';
elements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
@@ -609,7 +630,9 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
const prevElem = elements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (already handles override)
const coloredSep = applyColorsWithOverride(defaultSep, prevElem.item.color, prevElem.item.backgroundColor, prevElem.item.bold);
// Use the item's color if set, otherwise get the default color for that item type
const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type);
const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
@@ -623,7 +646,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
@@ -631,11 +654,11 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
// Apply padding with colors (using overrides if set)
const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none');
if (padding && (elem.item?.backgroundColor || hasColorOverride)) {
// Apply colors to padding - applyColorsWithOverride will handle the overrides
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor);
finalElements.push(paddedContent);
} else {
@@ -652,7 +675,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX' || (elements[i] && elements[i]?.type === 'flex-separator')) {
@@ -676,7 +699,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
@@ -692,7 +715,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
} else {
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
// Treat flex separators as normal separators when width detection fails
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
@@ -784,8 +807,8 @@ async function renderStatusLine(data: StatusJSON) {
const lineItems = lines[i];
if (lineItems && lineItems.length > 0) {
const line = renderSingleLine(lineItems, settings, data, tokenMetrics, sessionDuration);
// Add zero-width non-joiner at start of second+ lines to prevent trimming
const outputLine = i > 0 ? '\u200C' + line : line;
// Replace all spaces with non-breaking spaces to prevent VSCode trimming
const outputLine = line.replace(/ /g, '\u00A0');
console.log(outputLine);
}
}
-11
View File
@@ -31,11 +31,6 @@ export type FlexMode = 'full' | 'full-minus-40' | 'full-until-compact';
export interface Settings {
items?: StatusItem[]; // Legacy single line support
lines?: StatusItem[][]; // Multiple lines (up to 3)
colors: {
model: string;
gitBranch: string;
separator: string;
};
flexMode?: FlexMode; // How to handle terminal width for flex separators
compactThreshold?: number; // Context percentage (50-99) for 'full-until-compact' mode
defaultSeparator?: string; // Default separator character to insert between items
@@ -86,11 +81,6 @@ export const DEFAULT_SETTINGS: Settings = {
}
]
],
colors: {
model: 'cyan',
gitBranch: 'magenta',
separator: 'gray',
},
flexMode: 'full-minus-40',
compactThreshold: 60,
};
@@ -167,7 +157,6 @@ function migrateOldSettings(old: any): Settings {
return {
lines: [items], // Put migrated items in first line
colors: old.colors || DEFAULT_SETTINGS.colors,
flexMode: DEFAULT_SETTINGS.flexMode,
compactThreshold: DEFAULT_SETTINGS.compactThreshold,
};
+34 -50
View File
@@ -19,6 +19,29 @@ function getPackageVersion(): string {
}
}
// Get default color for each item type (matching ccstatusline.ts defaults)
function getDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
// Check if terminal width detection is available
function canDetectTerminalWidth(): boolean {
try {
@@ -240,7 +263,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
// Apply the previous element's colors to the separator (with overrides)
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: prevElem.item.color;
: (prevElem.item.color || getDefaultColor(prevElem.item.type));
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: prevElem.item.backgroundColor;
@@ -836,27 +859,6 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
}
});
// Helper to get the default color for each item type
const getDefaultColor = (type: string): string => {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'dim';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
default: return 'white';
}
};
const getItemDisplay = (item: StatusItem) => {
// Get the color for this item (use custom color if set, otherwise default)
@@ -1017,28 +1019,6 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(colorableItems[0]?.id || null);
const [editingBackground, setEditingBackground] = useState(false);
// Get default color for each item type (matching ccstatusline.ts defaults)
const getDefaultColor = (type: string): string => {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
};
// Handle keyboard input
const hasNoItems = colorableItems.length === 0;
@@ -1239,9 +1219,11 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
initialIndex={0}
/>
</Box>
<Box marginTop={1}>
<Text color='yellow'> VSCode users: </Text>
<Text dimColor>If colors appear incorrect, your VSCode theme may be overriding them.</Text>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'> VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
);
@@ -1613,9 +1595,11 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
<Text dimColor wrap='wrap'>
• Override colors: All items will use these colors instead of their configured colors
</Text>
<Box marginTop={1}>
<Text color='yellow'>⚠ VSCode users: </Text>
<Text dimColor>If colors appear incorrect, your VSCode theme may be overriding them.</Text>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'>⚠ VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
</>