Background colors, bold toggle, global options

This commit is contained in:
Matthew Breedlove committed 2025-08-11 15:33:06 -04:00
1 parent fd4f92230b
commit 6eafe54614
6 files changed
+688 -142

No files matched your search

+32
View File
@@ -4,12 +4,20 @@
![Demo](https://raw.githubusercontent.com/sirmalloc/ccstatusline/main/screenshots/demo.gif)
## 🆕 Recent Updates
### v1.1.0 - Enhanced Customization
- **🎨 Background Colors** - Set background colors for individual status items in Color Configuration menu
- **🔤 Bold Text Styling** - Toggle bold formatting for individual items in Color Configuration menu
- **⚙️ Global Options Menu** - New menu for global formatting preferences including padding, separators, color inheritance, and overrides
## ✨ Features
- **📊 Real-time Metrics** - Display model name, git branch, token usage, session duration, and more
- **🎨 Fully Customizable** - Choose what to display and customize colors for each element
- **📐 Multi-line Support** - Configure up to 3 independent status lines
- **🖥️ Interactive TUI** - Built-in configuration interface using React/Ink
- **⚙️ Global Options** - Apply consistent formatting across all items (padding, separators, bold, background)
- **🚀 Cross-platform** - Works seamlessly with both Bun and Node.js
- **📏 Smart Width Detection** - Automatically adapts to terminal width with flex separators
- **⚡ Zero Config** - Sensible defaults that work out of the box
@@ -76,6 +84,7 @@ Once configured, ccstatusline automatically formats your Claude Code status line
- **d** - Delete selected item
- **c** - Clear entire line
- **r** - Toggle raw value mode (no labels)
- **b** - Toggle bold text for this item
- **e** - Edit value (for custom-text and custom-command items)
- **w** - Set max width (for custom-command items)
- **t** - Set timeout in milliseconds (for custom-command items)
@@ -94,6 +103,29 @@ Configure how flex separators calculate available width:
- **Full width minus 40** - Leaves space for auto-compact message (default)
- **Full width until compact** - Switches based on context percentage threshold
### ⚙️ Global Options
Configure global formatting preferences that apply to all status items:
![Global Options](https://raw.githubusercontent.com/sirmalloc/ccstatusline/main/screenshots/global.png)
#### Default Padding & Separators
- **Default Padding** - Add consistent padding to the left and right of each item
- **Default Separator** - Automatically insert a separator between all items
- Press **(p)** to edit padding
- Press **(s)** to edit separator
#### Global Formatting
- **Inherit Colors** - Default separators inherit foreground and background colors from the preceding widget
- Press **(i)** to toggle
- **Global Bold** - Apply bold formatting to all text regardless of individual item settings
- Press **(o)** to toggle
- **Override Background Color** - Force all items to use the same background color
- Press **(b)** to cycle through colors
- Press **(c)** to clear override
> 💡 **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.
### 🔤 Raw Value Mode
Some items support "raw value" mode which displays just the value without a label:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "1.0.16",
"version": "1.1.0",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

+123 -50
View File
@@ -12,6 +12,32 @@ const readFile = fs.promises?.readFile || promisify(fs.readFile);
// Force chalk to use colors even when piped
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;
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
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;
}
interface StatusJSON {
session_id: string;
transcript_path: string;
@@ -307,6 +333,14 @@ 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
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
const bgColor = settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: backgroundColor;
const shouldBold = settings.globalBold || bold;
return applyColors(text, foregroundColor, bgColor, shouldBold);
};
const detectedWidth = getTerminalWidth();
// Calculate terminal width based on flex mode settings
let terminalWidth: number | null = null;
@@ -334,7 +368,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
}
}
}
const elements: { content: string, type: string }[] = [];
const elements: { content: string, type: string, item?: StatusItem }[] = [];
let hasFlexSeparator = false;
// Helper function to format token counts
@@ -349,77 +383,68 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
switch (item.type) {
case 'model':
if (data.model) {
const color = (chalk as any)[item.color || settings.colors.model] || chalk.cyan;
const text = item.rawValue ? data.model.display_name : `Model: ${data.model.display_name}`;
elements.push({ content: color(text), type: 'model' });
elements.push({ content: applyColorsWithOverride(text, item.color || settings.colors.model, item.backgroundColor, item.bold), type: 'model', item });
}
break;
case 'git-branch':
const branch = getGitBranch();
if (branch) {
const color = (chalk as any)[item.color || settings.colors.gitBranch] || chalk.magenta;
const text = item.rawValue ? branch : `⎇ ${branch}`;
elements.push({ content: color(text), type: 'git-branch' });
elements.push({ content: applyColorsWithOverride(text, item.color || settings.colors.gitBranch, item.backgroundColor, item.bold), type: 'git-branch', item });
}
break;
case 'git-changes':
const changes = getGitChanges();
if (changes !== null) {
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
// Compact format: (+42,-10)
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
elements.push({ content: color(changeStr), type: 'git-changes' });
elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
}
break;
case 'tokens-input':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
const text = item.rawValue ? formatTokens(tokenMetrics.inputTokens) : `In: ${formatTokens(tokenMetrics.inputTokens)}`;
elements.push({ content: color(text), type: 'tokens-input' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'green'] || chalk.green;
const text = item.rawValue ? formatTokens(tokenMetrics.outputTokens) : `Out: ${formatTokens(tokenMetrics.outputTokens)}`;
elements.push({ content: color(text), type: 'tokens-output' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
const text = item.rawValue ? formatTokens(tokenMetrics.cachedTokens) : `Cached: ${formatTokens(tokenMetrics.cachedTokens)}`;
elements.push({ content: color(text), type: 'tokens-cached' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'white'] || chalk.white;
const text = item.rawValue ? formatTokens(tokenMetrics.totalTokens) : `Total: ${formatTokens(tokenMetrics.totalTokens)}`;
elements.push({ content: color(text), type: 'tokens-total' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
const text = item.rawValue ? formatTokens(tokenMetrics.contextLength) : `Ctx: ${formatTokens(tokenMetrics.contextLength)}`;
elements.push({ content: color(text), type: 'context-length' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
case 'context-percentage':
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: color(text), type: 'context-percentage' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
@@ -427,64 +452,60 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
if (tokenMetrics) {
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (tokenMetrics.contextLength / 160000) * 100);
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: color(text), type: 'context-percentage-usable' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const detectedWidth = terminalWidth || getTerminalWidth();
if (detectedWidth) {
const color = (chalk as any)[item.color || 'dim'] || chalk.dim;
const text = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
elements.push({ content: color(text), type: 'terminal-width' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
case 'session-clock':
if (sessionDuration) {
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
const text = item.rawValue ? sessionDuration : `Session: ${sessionDuration}`;
elements.push({ content: color(text), type: 'session-clock' });
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
case 'version':
const versionString = data.version || 'Unknown';
const versionColor = (chalk as any)[item.color || 'green'] || chalk.green;
const text = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: versionColor(text), type: 'version' });
const versionText = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
break;
case 'separator':
// Only add separator if there are already elements and the last one isn't a separator
const lastElement = elements[elements.length - 1];
if (elements.length > 0 && lastElement && lastElement.type !== 'separator') {
const sepColor = (chalk as any)[settings.colors.separator] || chalk.dim;
const sepChar = item.character || '|';
// Handle special separator cases
let sepContent;
let sepText;
if (sepChar === ',') {
sepContent = sepColor(`${sepChar} `);
sepText = `${sepChar} `;
} else if (sepChar === ' ') {
sepContent = sepColor(' ');
sepText = ' ';
} else {
sepContent = sepColor(` ${sepChar} `);
sepText = ` ${sepChar} `;
}
elements.push({ content: sepContent, type: 'separator' });
// 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);
elements.push({ content: sepContent, type: 'separator', item });
}
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator' });
elements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customTextColor = (chalk as any)[item.color || 'white'] || chalk.white;
const customText = item.customText || '';
elements.push({ content: customTextColor(customText), type: 'custom-text' });
elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item });
break;
case 'custom-command':
@@ -542,11 +563,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, '');
const cmdColor = (chalk as any)[item.color || 'white'] || chalk.white;
elements.push({ content: cmdColor(stripped), type: 'custom-command' });
elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// Preserve original colors from command output - ignore any color property
elements.push({ content: finalOutput, type: 'custom-command' });
elements.push({ content: finalOutput, type: 'custom-command', item });
}
}
} catch {
@@ -563,21 +583,71 @@ 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) {
// Check if we should inherit colors from the previous element
if (settings.inheritSeparatorColors && index > 0) {
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);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
} else if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
// Apply override background even when not inheriting colors
const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
} else {
// Apply padding with the same background color as the item (or override)
const bgColor = settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: elem.item?.backgroundColor;
if (padding && bgColor) {
// Apply background color to padding
const paddedContent = applyColorsWithOverride(padding, undefined, bgColor) +
elem.content +
applyColorsWithOverride(padding, undefined, bgColor);
finalElements.push(paddedContent);
} else {
// No background color or no padding
finalElements.push(padding + elem.content + padding);
}
}
});
// Build the final status line
let statusLine = '';
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (const elem of elements) {
if (elem.type === 'flex-separator') {
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX' || (elements[i] && elements[i].type === 'flex-separator')) {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]!.push(elem.content);
parts[currentPart]!.push(elem);
}
}
@@ -611,10 +681,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
statusLine = elements.map(e => e.type === 'flex-separator' ? chalk.dim(' | ') : e.content).join('');
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
statusLine = elements.map(e => e.content).join('');
statusLine = finalElements.join('');
}
}
@@ -698,10 +768,13 @@ async function renderStatusLine(data: StatusJSON) {
}
// Render each line
for (const lineItems of lines) {
for (let i = 0; i < lines.length; i++) {
const lineItems = lines[i];
if (lineItems.length > 0) {
const line = renderSingleLine(lineItems, settings, data, tokenMetrics, sessionDuration);
console.log(line);
// Add zero-width non-joiner at start of second+ lines to prevent trimming
const outputLine = i > 0 ? '\u200C' + line : line;
console.log(outputLine);
}
}
}
@@ -729,4 +802,4 @@ async function main() {
}
}
main();
main();
+9 -2
View File
@@ -15,6 +15,8 @@ export interface StatusItem {
id: string;
type: StatusItemType;
color?: string;
backgroundColor?: string; // Background color for the item
bold?: boolean; // Bold text styling
character?: string; // For separator and flex-separator types
rawValue?: boolean; // Show value without label prefix
customText?: string; // For custom-text type
@@ -36,6 +38,11 @@ export interface Settings {
};
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
defaultPadding?: string; // Default padding to add around all items
inheritSeparatorColors?: boolean; // Whether default separators inherit colors from preceding widget
overrideBackgroundColor?: string; // Override background color for all items (e.g., 'none', 'bgRed', etc.)
globalBold?: boolean; // Apply bold formatting to all items
}
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccstatusline');
@@ -56,7 +63,7 @@ export const DEFAULT_SETTINGS: Settings = {
{
"id": "3",
"type": "context-length",
"color": "dim"
"color": "gray"
},
{
"id": "4",
@@ -81,7 +88,7 @@ export const DEFAULT_SETTINGS: Settings = {
colors: {
model: 'cyan',
gitBranch: 'magenta',
separator: 'dim',
separator: 'gray',
},
flexMode: 'full-minus-40',
compactThreshold: 60,
+523 -89
View File
@@ -69,7 +69,41 @@ interface StatusLinePreviewProps {
settings?: Settings;
}
// Helper function to apply foreground, background colors and bold
const applyColors = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
let result = text;
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
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;
};
const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetectionAvailable: boolean, settings?: Settings): string => {
// Helper to apply colors with optional background override
const applyItemColors = (text: string, item: StatusItem, defaultColor?: string): string => {
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: item.backgroundColor;
const shouldBold = settings?.globalBold || item.bold;
return applyColors(text, item.color || defaultColor, bgColor, shouldBold);
};
// Calculate effective width based on flex mode settings
let effectiveWidth: number | null = null;
if (widthDetectionAvailable && terminalWidth) {
@@ -87,100 +121,153 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
// Always ensure we have a width for truncation
const width = terminalWidth; // Always use terminal width for truncation
const flexWidth = effectiveWidth; // Use this for flex separator calculations
const elements: string[] = [];
const rawElements: { content: string; type: string; item?: StatusItem }[] = [];
let hasFlexSeparator = false;
items.forEach(item => {
items.forEach((item, index) => {
switch (item.type) {
case 'model':
const modelColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push(modelColor(item.rawValue ? 'Claude' : 'Model: Claude'));
const modelText = item.rawValue ? 'Claude' : 'Model: Claude';
rawElements.push({ content: applyItemColors(modelText, item, 'cyan'), type: 'model', item });
break;
case 'git-branch':
const branchColor = (chalk as any)[item.color || 'magenta'] || chalk.magenta;
elements.push(branchColor('⎇ main'));
const branchText = '⎇ main';
rawElements.push({ content: applyItemColors(branchText, item, 'magenta'), type: 'git-branch', item });
break;
case 'git-changes':
const changesColor = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
elements.push(changesColor('(+42,-10)'));
const changesText = '(+42,-10)';
rawElements.push({ content: applyItemColors(changesText, item, 'yellow'), type: 'git-changes', item });
break;
case 'tokens-input':
const inputColor = (chalk as any)[item.color || 'blue'] || chalk.blue;
elements.push(inputColor(item.rawValue ? '15.2k' : 'In: 15.2k'));
const inputText = item.rawValue ? '15.2k' : 'In: 15.2k';
rawElements.push({ content: applyItemColors(inputText, item, 'blue'), type: 'tokens-input', item });
break;
case 'tokens-output':
const outputColor = (chalk as any)[item.color || 'white'] || chalk.white;
elements.push(outputColor(item.rawValue ? '3.4k' : 'Out: 3.4k'));
const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k';
rawElements.push({ content: applyItemColors(outputText, item, 'white'), type: 'tokens-output', item });
break;
case 'tokens-cached':
const cachedColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push(cachedColor(item.rawValue ? '12k' : 'Cached: 12k'));
const cachedText = item.rawValue ? '12k' : 'Cached: 12k';
rawElements.push({ content: applyItemColors(cachedText, item, 'cyan'), type: 'tokens-cached', item });
break;
case 'tokens-total':
const totalColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push(totalColor(item.rawValue ? '30.6k' : 'Total: 30.6k'));
const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k';
rawElements.push({ content: applyItemColors(totalText, item, 'cyan'), type: 'tokens-total', item });
break;
case 'context-length':
const ctxColor = (chalk as any)[item.color || 'dim'] || chalk.dim;
elements.push(ctxColor(item.rawValue ? '18.6k' : 'Ctx: 18.6k'));
const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k';
rawElements.push({ content: applyItemColors(ctxText, item, 'gray'), type: 'context-length', item });
break;
case 'context-percentage':
const ctxPctColor = (chalk as any)[item.color || 'blue'] || chalk.blue;
elements.push(ctxPctColor(item.rawValue ? '9.3%' : 'Ctx: 9.3%'));
const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%';
rawElements.push({ content: applyItemColors(ctxPctText, item, 'blue'), type: 'context-percentage', item });
break;
case 'context-percentage-usable':
const ctxUsableColor = (chalk as any)[item.color || 'green'] || chalk.green;
elements.push(ctxUsableColor(item.rawValue ? '11.6%' : 'Ctx(u): 11.6%'));
const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
rawElements.push({ content: applyItemColors(ctxUsableText, item, 'green'), type: 'context-percentage-usable', item });
break;
case 'session-clock':
const sessionColor = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
elements.push(sessionColor(item.rawValue ? '2hr 15m' : 'Session: 2hr 15m'));
const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
rawElements.push({ content: applyItemColors(sessionText, item, 'yellow'), type: 'session-clock', item });
break;
case 'version':
const versionColor = (chalk as any)[item.color || 'green'] || chalk.green;
elements.push(versionColor(item.rawValue ? '1.0.72' : 'Version: 1.0.72'));
const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72';
rawElements.push({ content: applyItemColors(versionText, item, 'green'), type: 'version', item });
break;
case 'terminal-width':
const termColor = (chalk as any)[item.color || 'gray'] || chalk.gray;
const detectedWidth = canDetectTerminalWidth() ? terminalWidth : '??';
elements.push(termColor(item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`));
const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
rawElements.push({ content: applyItemColors(termText, item, 'gray'), type: 'terminal-width', item });
break;
case 'separator':
const sepChar = item.character || '|';
// Handle special separator cases
let sepContent;
if (sepChar === ',') {
sepContent = chalk.dim(`${sepChar} `);
sepContent = `${sepChar} `;
} else if (sepChar === ' ') {
sepContent = chalk.dim(' ');
sepContent = ' ';
} else {
sepContent = chalk.dim(` ${sepChar} `);
sepContent = ` ${sepChar} `;
}
elements.push(sepContent);
rawElements.push({ content: applyItemColors(sepContent, item, 'gray'), type: 'separator', item });
break;
case 'flex-separator':
elements.push('FLEX');
rawElements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customTextColor = (chalk as any)[item.color || 'white'] || chalk.white;
const customText = item.customText || '';
elements.push(customTextColor(customText));
rawElements.push({ content: applyItemColors(customText, item, 'white'), type: 'custom-text', item });
break;
case 'custom-command':
const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
// Only apply color if not preserving colors
if (!item.preserveColors) {
const cmdColor = (chalk as any)[item.color || 'white'] || chalk.white;
elements.push(cmdColor(cmdText));
rawElements.push({ content: applyItemColors(cmdText, item, 'white'), type: 'custom-command', item });
} else {
// When preserving colors, just show the text without color
elements.push(chalk.white(cmdText));
// When preserving colors, just show the text without applying our colors
rawElements.push({ content: cmdText, type: 'custom-command', item });
}
break;
}
});
// Apply default padding and separators
const elements: string[] = [];
const padding = settings?.defaultPadding || '';
const defaultSep = settings?.defaultSeparator || '';
// Debug: Check if we have a default separator
// console.log('Default separator:', JSON.stringify(defaultSep), 'Length:', defaultSep.length);
rawElements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
// Check if we should inherit colors from the previous element
if (settings?.inheritSeparatorColors && index > 0) {
const prevElem = rawElements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (with override)
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: prevElem.item.backgroundColor;
const coloredSep = applyColors(defaultSep, prevElem.item.color, bgColor, prevElem.item.bold);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
} else if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
// Apply override background even when not inheriting colors
const coloredSep = applyColors(defaultSep, undefined, settings.overrideBackgroundColor);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
}
// Add the element itself
if (elem.type === 'separator' || elem.type === 'flex-separator') {
elements.push(elem.content);
} else {
// Apply padding with the same background color as the item (or override)
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: elem.item?.backgroundColor;
if (padding && bgColor) {
// Apply background color to padding
const paddedContent = applyColors(padding, undefined, bgColor) +
elem.content +
applyColors(padding, undefined, bgColor);
elements.push(paddedContent);
} else {
// No background color or no padding
elements.push(padding + elem.content + padding);
}
}
});
// Build the status line with flex separator support
let statusLine = '';
if (hasFlexSeparator && flexWidth) {
@@ -228,7 +315,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
}
} else {
// No width detection available - treat flex separators as normal separators
statusLine = elements.map(e => e === 'FLEX' ? chalk.dim(' | ') : e).join('');
statusLine = elements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
}
// Truncate if the line exceeds the maximum width
@@ -278,9 +365,9 @@ const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ lines, terminalWi
const widthDetectionAvailable = canDetectTerminalWidth();
// Build the Claude Code input box - account for ink's padding
const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
const topLine = chalk.dim('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮');
const middleLine = chalk.dim('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.dim('│');
const bottomLine = chalk.dim('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯');
const topLine = chalk.gray('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮');
const middleLine = chalk.gray('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.gray('│');
const bottomLine = chalk.gray('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯');
// Render each configured line - account for 2-space prefix in display
const availableWidth = boxWidth - 2; // Account for 2-space indent
@@ -331,7 +418,14 @@ interface LineSelectorProps {
onBack: () => void;
}
const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack }) => {
interface LineSelectorProps {
lines: StatusItem[][];
onSelect: (line: number) => void;
onBack: () => void;
initialSelection?: number;
}
const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack, initialSelection = 0 }) => {
const items = [
{ label: `📝 Line 1${lines[0] && lines[0].length > 0 ? ` (${lines[0].length} items)` : ' (empty)'}`, value: 0 },
{ label: `📝 Line 2${lines[1] && lines[1].length > 0 ? ` (${lines[1].length} items)` : ' (empty)'}`, value: 1 },
@@ -363,6 +457,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack })
<SelectInput
items={items}
onSelect={handleSelect}
initialIndex={Math.min(initialSelection, items.length - 1)}
/>
</Box>
</Box>
@@ -373,13 +468,15 @@ interface MainMenuProps {
onSelect: (value: string) => void;
isClaudeInstalled: boolean;
hasChanges: boolean;
initialSelection?: number;
}
const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges }) => {
const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges, initialSelection = 0 }) => {
const items = [
{ label: '📝 Edit Lines', value: 'lines' },
{ label: '🎨 Configure Colors', value: 'colors' },
{ label: '⚡ Flex Options', value: 'flex' },
{ label: '🔧 Global Options', value: 'globalOptions' },
{ label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' },
];
@@ -396,7 +493,11 @@ const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasCha
<Box flexDirection='column'>
<Text bold>Main Menu</Text>
<Box marginTop={1}>
<SelectInput items={items} onSelect={(item) => onSelect(item.value)} />
<SelectInput
items={items}
onSelect={(item) => onSelect(item.value)}
initialIndex={Math.min(initialSelection, items.length - 1)}
/>
</Box>
</Box>
);
@@ -623,14 +724,16 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Enter move mode
setMoveMode(true);
} else if (input === 'a') {
// Add item at end
// Add item after selected
const newItem: StatusItem = {
id: Date.now().toString(),
type: 'separator',
};
const newItems = [...items, newItem];
const newItems = [...items];
const insertIndex = items.length > 0 ? selectedIndex + 1 : 0;
newItems.splice(insertIndex, 0, newItem);
onUpdate(newItems);
setSelectedIndex(newItems.length - 1); // Move selection to new item
setSelectedIndex(insertIndex); // Move selection to new item
} else if (input === 'i') {
// Insert item before selected
const newItem: StatusItem = {
@@ -750,7 +853,8 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
case 'separator': {
const char = item.character || '|';
const charDisplay = char === ' ' ? '(space)' : char;
return chalk.dim(`Separator ${charDisplay}`);
// Apply the separator's color to its display
return applyColors(`Separator ${charDisplay}`, item.color || 'gray', item.backgroundColor, item.bold);
}
case 'flex-separator':
return chalk.yellow('Flex Separator');
@@ -890,7 +994,8 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors) // Exclude custom-command items with preserveColors
);
const [selectedIndex, setSelectedIndex] = useState(0);
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 => {
@@ -904,35 +1009,77 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'dim';
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 ESC key
// Handle keyboard input
const hasNoItems = colorableItems.length === 0;
useInput((input, key) => {
if (key.escape) {
// If no items, any key goes back
if (hasNoItems) {
onBack();
return;
}
// Normal keyboard handling when there are items
if (key.escape) {
if (editingBackground) {
setEditingBackground(false);
} else {
onBack();
}
} else if (input === 'f' || input === 'F') {
if (colorableItems.length > 0) {
setEditingBackground(!editingBackground);
}
} else if (input === 'b' || input === 'B') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Toggle bold for the highlighted item
const selectedItem = colorableItems.find(item => item.id === highlightedItemId);
if (selectedItem) {
const newItems = items.map(item => {
if (item.id === selectedItem.id) {
return { ...item, bold: !item.bold };
}
return item;
});
onUpdate(newItems);
}
}
} else if (input === 'r' || input === 'R') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Reset all styling (color, background, and bold) for the highlighted item
const selectedItem = colorableItems.find(item => item.id === highlightedItemId);
if (selectedItem) {
const newItems = items.map(item => {
if (item.id === selectedItem.id) {
// Remove color, backgroundColor, and bold properties
const { color, backgroundColor, bold, ...restItem } = item;
return restItem;
}
return item;
});
onUpdate(newItems);
}
}
}
});
if (colorableItems.length === 0) {
if (hasNoItems) {
return (
<Box flexDirection='column'>
<Text bold>Configure Colors</Text>
<Box marginTop={1}><Text dimColor>No colorable items in the status line.</Text></Box>
<Text dimColor>Add a Model or Git Branch item first.</Text>
<Box marginTop={1}><Text>Press any key to go back...</Text></Box>
{/* Press any key handler */}
{(() => {
useInput(() => { onBack(); });
return null;
})()}
</Box>
);
}
@@ -958,16 +1105,22 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const timeout = item.timeout ? ` ${item.timeout}ms` : '';
return `Custom Command (${cmd}${timeout})`;
}
case 'separator': {
const char = item.character || '|';
const charDisplay = char === ' ' ? '(space)' : char;
return `Separator ${charDisplay}`;
}
default: return item.type;
}
};
// Create menu items with colored labels
const menuItems = colorableItems.map((item, index) => {
const color = item.color || getDefaultColor(item.type);
const colorFunc = (chalk as any)[color] || chalk.white;
const label = `${index + 1}: ${getItemLabel(item)}`;
// Apply both foreground and background colors
const styledLabel = applyColors(label, item.color || getDefaultColor(item.type), item.backgroundColor, item.bold);
return {
label: colorFunc(`${index + 1}: ${getItemLabel(item)}`),
label: styledLabel,
value: item.id,
};
});
@@ -980,9 +1133,25 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
// Cycle through colors
const newItems = items.map(item => {
if (item.id === selected.value) {
const currentColorIndex = colors.indexOf(item.color || 'white');
const nextColor = colors[(currentColorIndex + 1) % colors.length];
return { ...item, color: nextColor };
if (editingBackground) {
const currentBgColor = item.backgroundColor || 'none';
let currentBgColorIndex = bgColors.indexOf(currentBgColor);
// If color not found, start from beginning
if (currentBgColorIndex === -1) currentBgColorIndex = 0;
const nextBgColor = bgColors[(currentBgColorIndex + 1) % bgColors.length];
return { ...item, backgroundColor: nextBgColor === 'none' ? undefined : nextBgColor };
} else {
let currentColor = item.color || getDefaultColor(item.type);
// If color is 'dim', treat as if no color was set
if (currentColor === 'dim') {
currentColor = getDefaultColor(item.type);
}
let currentColorIndex = colors.indexOf(currentColor);
// If color not found, start from beginning
if (currentColorIndex === -1) currentColorIndex = 0;
const nextColor = colors[(currentColorIndex + 1) % colors.length];
return { ...item, color: nextColor };
}
}
return item;
});
@@ -991,33 +1160,55 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
};
const handleHighlight = (item: { value: string }) => {
if (item.value !== 'back') {
const itemIndex = colorableItems.findIndex(i => i.id === item.value);
if (itemIndex !== -1) {
setSelectedIndex(itemIndex);
}
}
setHighlightedItemId(item.value);
};
// Color list for cycling
const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
'magentaBright', 'cyanBright', 'whiteBright'];
// Background colors list (includes 'none' as first option)
// Note: chalk doesn't support bgDim, so we exclude it from background colors
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
// Get current color for selected item (if a valid colorable item is selected)
const selectedItem = selectedIndex < colorableItems.length ? colorableItems[selectedIndex] : null;
const currentColor = selectedItem ? (selectedItem.color || getDefaultColor(selectedItem.type)) : 'white';
const colorIndex = colors.indexOf(currentColor);
const colorNumber = colorIndex === -1 ? 8 : colorIndex + 1; // Default to white (8) if not found
const colorDisplay = (chalk as any)[currentColor] ? (chalk as any)[currentColor](currentColor) : chalk.white(currentColor);
// Get current color for highlighted item
const selectedItem = highlightedItemId && highlightedItemId !== 'back'
? colorableItems.find(item => item.id === highlightedItemId)
: null;
const currentColor = editingBackground
? (selectedItem?.backgroundColor || 'none')
: (selectedItem ? (selectedItem.color || getDefaultColor(selectedItem.type)) : 'white');
const colorList = editingBackground ? bgColors : colors;
const colorIndex = colorList.indexOf(currentColor);
const colorNumber = colorIndex === -1 ? (editingBackground ? 1 : 8) : colorIndex + 1;
let colorDisplay;
if (editingBackground) {
if (currentColor === 'none') {
colorDisplay = chalk.gray('(no background)');
} else {
const bgColorName = currentColor.replace(/^bg/, '').toLowerCase();
const bgFunc = (chalk as any)[currentColor];
colorDisplay = bgFunc ? bgFunc(` ${bgColorName} `) : chalk.white(currentColor);
}
} else {
colorDisplay = (chalk as any)[currentColor] ? (chalk as any)[currentColor](currentColor) : chalk.white(currentColor);
}
return (
<Box flexDirection='column'>
<Text bold>Configure Colors</Text>
<Text dimColor>↑↓ to select item, Enter to cycle color, ESC to go back</Text>
<Text bold>Configure Colors {editingBackground && chalk.yellow('[Background Mode]')}</Text>
<Text dimColor>↑↓ to select, Enter to cycle {editingBackground ? 'background' : 'foreground'}, (f) to toggle bg/fg, (b)old, (r)eset, ESC to go back</Text>
{selectedItem && (
<Box marginTop={1}>
<Text>Current color ({colorNumber}/{colors.length}): {colorDisplay}</Text>
<Text>
Current {editingBackground ? 'background' : 'foreground'} ({colorNumber}/{colorList.length}): {colorDisplay}
{selectedItem.bold && chalk.bold(' [BOLD]')}
</Text>
</Box>
)}
<Box marginTop={1}>
@@ -1025,7 +1216,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
items={menuItems}
onSelect={handleSelect}
onHighlight={handleHighlight}
initialIndex={selectedIndex}
initialIndex={0}
/>
</Box>
</Box>
@@ -1183,13 +1374,199 @@ const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack })
);
};
interface GlobalOptionsMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdate, onBack }) => {
const [editingPadding, setEditingPadding] = useState(false);
const [editingSeparator, setEditingSeparator] = useState(false);
const [paddingInput, setPaddingInput] = useState(settings.defaultPadding || '');
const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator || '');
const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors || false);
const [globalBold, setGlobalBold] = useState(settings.globalBold || false);
// Background color override
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
const currentBgIndex = bgColors.indexOf(settings.overrideBackgroundColor || 'none');
useInput((input, key) => {
if (editingPadding) {
if (key.return) {
const updatedSettings = {
...settings,
defaultPadding: paddingInput
};
onUpdate(updatedSettings);
setEditingPadding(false);
} else if (key.escape) {
setPaddingInput(settings.defaultPadding || '');
setEditingPadding(false);
} else if (key.backspace || key.delete) {
setPaddingInput(paddingInput.slice(0, -1));
} else if (input) {
setPaddingInput(paddingInput + input);
}
} else if (editingSeparator) {
if (key.return) {
const updatedSettings = {
...settings,
defaultSeparator: separatorInput
};
onUpdate(updatedSettings);
setEditingSeparator(false);
} else if (key.escape) {
setSeparatorInput(settings.defaultSeparator || '');
setEditingSeparator(false);
} else if (key.backspace || key.delete) {
setSeparatorInput(separatorInput.slice(0, -1));
} else if (input) {
setSeparatorInput(separatorInput + input);
}
} else {
if (key.escape) {
onBack();
} else if (input === 'p' || input === 'P') {
setEditingPadding(true);
} else if (input === 's' || input === 'S') {
setEditingSeparator(true);
} else if (input === 'i' || input === 'I') {
const newInheritColors = !inheritColors;
setInheritColors(newInheritColors);
const updatedSettings = {
...settings,
inheritSeparatorColors: newInheritColors
};
onUpdate(updatedSettings);
} else if (input === 'b' || input === 'B') {
// Cycle through background colors
const nextIndex = (currentBgIndex + 1) % bgColors.length;
const nextBgColor = bgColors[nextIndex];
const updatedSettings = {
...settings,
overrideBackgroundColor: nextBgColor === 'none' ? undefined : nextBgColor
};
onUpdate(updatedSettings);
} else if (input === 'c' || input === 'C') {
// Clear override background color
const updatedSettings = {
...settings,
overrideBackgroundColor: undefined
};
onUpdate(updatedSettings);
} else if (input === 'o' || input === 'O') {
// Toggle global bold
const newGlobalBold = !globalBold;
setGlobalBold(newGlobalBold);
const updatedSettings = {
...settings,
globalBold: newGlobalBold
};
onUpdate(updatedSettings);
}
}
});
return (
<Box flexDirection='column'>
<Text bold>Global Options</Text>
<Text dimColor>Configure automatic padding and separators between items</Text>
<Box marginTop={1} />
{editingPadding ? (
<Box flexDirection="column">
<Box>
<Text>Enter default padding (applied to left and right of each item): </Text>
<Text color="cyan">{paddingInput ? `"${paddingInput}"` : '(empty)'}</Text>
</Box>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
) : editingSeparator ? (
<Box flexDirection="column">
<Box>
<Text>Enter default separator (placed between items): </Text>
<Text color="cyan">{separatorInput ? `"${separatorInput}"` : '(empty - no separator will be added)'}</Text>
</Box>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
) : (
<>
<Box marginTop={1}>
<Text> Default Padding: </Text>
<Text color="cyan">{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
<Text dimColor> - Press (p) to edit</Text>
</Box>
<Box marginTop={1}>
<Text>Default Separator: </Text>
<Text color="cyan">{settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'}</Text>
<Text dimColor> - Press (s) to edit</Text>
</Box>
<Box marginTop={1}>
<Text> Inherit Colors: </Text>
<Text color={inheritColors ? "green" : "red"}>{inheritColors ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (i) to toggle</Text>
</Box>
<Box marginTop={1}>
<Text> Global Bold: </Text>
<Text color={globalBold ? "green" : "red"}>{globalBold ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (o) to toggle</Text>
</Box>
<Box marginTop={1}>
<Text>Override BG Color: </Text>
{(() => {
const bgColor = settings.overrideBackgroundColor || 'none';
if (bgColor === 'none') {
return <Text color="gray">(none)</Text>;
} else {
const bgColorName = bgColor.replace(/^bg/, '').toLowerCase();
const bgFunc = (chalk as any)[bgColor];
const display = bgFunc ? bgFunc(` ${bgColorName} `) : bgColorName;
return <Text>{display}</Text>;
}
})()}
<Text dimColor> - (b) cycle, (c) clear</Text>
</Box>
<Box marginTop={2}>
<Text dimColor>Press ESC to go back</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Text dimColor wrap='wrap'>
Note: These settings are applied during rendering and don't add items to your widget list.
</Text>
<Text dimColor wrap='wrap'>
• Inherit colors: Default separators will use colors from the preceding widget
</Text>
<Text dimColor wrap='wrap'>
• Global Bold: Makes all text bold regardless of individual settings
</Text>
<Text dimColor wrap='wrap'>
• Override BG: All items will use this background color instead of their configured colors
</Text>
</Box>
</>
)}
</Box>
);
};
const App: React.FC = () => {
const { exit } = useApp();
const [settings, setSettings] = useState<Settings | null>(null);
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
const [hasChanges, setHasChanges] = useState(false);
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'flex' | 'confirm'>('main');
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'flex' | 'globalOptions' | 'confirm'>('main');
const [selectedLine, setSelectedLine] = useState(0);
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
@@ -1286,6 +1663,9 @@ const App: React.FC = () => {
case 'flex':
setScreen('flex');
break;
case 'globalOptions':
setScreen('globalOptions');
break;
case 'install':
await handleInstallUninstall();
break;
@@ -1324,19 +1704,52 @@ const App: React.FC = () => {
<StatusLinePreview lines={settings.lines || [[]]} terminalWidth={terminalWidth} settings={settings} />
<Box marginTop={2}>
{screen === 'main' && <MainMenu onSelect={handleMainMenuSelect} isClaudeInstalled={isClaudeInstalled} hasChanges={hasChanges} />}
{screen === 'main' && (
<MainMenu
onSelect={(value) => {
// Only persist menu selection if not exiting
if (value !== 'save' && value !== 'exit') {
const menuMap: Record<string, number> = {
'lines': 0,
'colors': 1,
'flex': 2,
'globalOptions': 3,
'install': 4
};
setMenuSelections({ ...menuSelections, main: menuMap[value] || 0 });
}
handleMainMenuSelect(value);
}}
isClaudeInstalled={isClaudeInstalled}
hasChanges={hasChanges}
initialSelection={menuSelections.main || 0}
/>
)}
{screen === 'lines' && (
<LineSelector
lines={settings.lines || [[]]}
onSelect={handleLineSelect}
onBack={() => setScreen('main')}
onSelect={(line) => {
setMenuSelections({ ...menuSelections, lines: line });
handleLineSelect(line);
}}
onBack={() => {
// Save that we came from 'lines' menu (index 0)
// Clear the line selection so it resets next time we enter
setMenuSelections({ ...menuSelections, main: 0, lines: undefined });
setScreen('main');
}}
initialSelection={menuSelections.lines || 0}
/>
)}
{screen === 'items' && settings.lines && (
<ItemsEditor
items={settings.lines[selectedLine] || []}
onUpdate={(items) => updateLine(selectedLine, items)}
onBack={() => setScreen('lines')}
onBack={() => {
// When going back to lines menu, preserve which line was selected
setMenuSelections({ ...menuSelections, lines: selectedLine });
setScreen('lines');
}}
lineNumber={selectedLine + 1}
/>
)}
@@ -1361,7 +1774,11 @@ const App: React.FC = () => {
}
setSettings({ ...settings, lines: newLines });
}}
onBack={() => setScreen('main')}
onBack={() => {
// Save that we came from 'colors' menu (index 1)
setMenuSelections({ ...menuSelections, main: 1 });
setScreen('main');
}}
/>
)}
{screen === 'flex' && (
@@ -1370,7 +1787,24 @@ const App: React.FC = () => {
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => setScreen('main')}
onBack={() => {
// Save that we came from 'flex' menu (index 2)
setMenuSelections({ ...menuSelections, main: 2 });
setScreen('main');
}}
/>
)}
{screen === 'globalOptions' && (
<GlobalOptionsMenu
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
// Save that we came from 'globalOptions' menu (index 3)
setMenuSelections({ ...menuSelections, main: 3 });
setScreen('main');
}}
/>
)}
{screen === 'confirm' && confirmDialog && (