diff --git a/README.md b/README.md index 5f2590a..f95fc5e 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/package.json b/package.json index 618fe92..5b15f4d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/screenshots/global.png b/screenshots/global.png new file mode 100644 index 0000000..3bfc578 Binary files /dev/null and b/screenshots/global.png differ diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 75b3caf..7c5dc13 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -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(); \ No newline at end of file +main(); diff --git a/src/config.ts b/src/config.ts index de5ebc3..8d6f892 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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, diff --git a/src/tui.tsx b/src/tui.tsx index 4cc2026..45dd338 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -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 = ({ 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 = ({ lines, onSelect, onBack }) => { +interface LineSelectorProps { + lines: StatusItem[][]; + onSelect: (line: number) => void; + onBack: () => void; + initialSelection?: number; +} + +const LineSelector: React.FC = ({ 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 = ({ lines, onSelect, onBack }) @@ -373,13 +468,15 @@ interface MainMenuProps { onSelect: (value: string) => void; isClaudeInstalled: boolean; hasChanges: boolean; + initialSelection?: number; } -const MainMenu: React.FC = ({ onSelect, isClaudeInstalled, hasChanges }) => { +const MainMenu: React.FC = ({ 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 = ({ onSelect, isClaudeInstalled, hasCha Main Menu - onSelect(item.value)} /> + onSelect(item.value)} + initialIndex={Math.min(initialSelection, items.length - 1)} + /> ); @@ -623,14 +724,16 @@ const ItemsEditor: React.FC = ({ 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 = ({ 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 = ({ 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(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 = ({ 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 ( Configure Colors No colorable items in the status line. Add a Model or Git Branch item first. Press any key to go back... - {/* Press any key handler */} - {(() => { - useInput(() => { onBack(); }); - return null; - })()} ); } @@ -958,16 +1105,22 @@ const ColorMenu: React.FC = ({ 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 = ({ 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 = ({ 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 ( - Configure Colors - ↑↓ to select item, Enter to cycle color, ESC to go back + Configure Colors {editingBackground && chalk.yellow('[Background Mode]')} + ↑↓ to select, Enter to cycle {editingBackground ? 'background' : 'foreground'}, (f) to toggle bg/fg, (b)old, (r)eset, ESC to go back {selectedItem && ( - Current color ({colorNumber}/{colors.length}): {colorDisplay} + + Current {editingBackground ? 'background' : 'foreground'} ({colorNumber}/{colorList.length}): {colorDisplay} + {selectedItem.bold && chalk.bold(' [BOLD]')} + )} @@ -1025,7 +1216,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { items={menuItems} onSelect={handleSelect} onHighlight={handleHighlight} - initialIndex={selectedIndex} + initialIndex={0} /> @@ -1183,13 +1374,199 @@ const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) ); }; +interface GlobalOptionsMenuProps { + settings: Settings; + onUpdate: (settings: Settings) => void; + onBack: () => void; +} + +const GlobalOptionsMenu: React.FC = ({ 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 ( + + Global Options + Configure automatic padding and separators between items + + + {editingPadding ? ( + + + Enter default padding (applied to left and right of each item): + {paddingInput ? `"${paddingInput}"` : '(empty)'} + + Press Enter to save, ESC to cancel + + ) : editingSeparator ? ( + + + Enter default separator (placed between items): + {separatorInput ? `"${separatorInput}"` : '(empty - no separator will be added)'} + + Press Enter to save, ESC to cancel + + ) : ( + <> + + Default Padding: + {settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'} + - Press (p) to edit + + + + Default Separator: + {settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'} + - Press (s) to edit + + + + Inherit Colors: + {inheritColors ? '✓ Enabled' : '✗ Disabled'} + - Press (i) to toggle + + + + Global Bold: + {globalBold ? '✓ Enabled' : '✗ Disabled'} + - Press (o) to toggle + + + + Override BG Color: + {(() => { + const bgColor = settings.overrideBackgroundColor || 'none'; + if (bgColor === 'none') { + return (none); + } else { + const bgColorName = bgColor.replace(/^bg/, '').toLowerCase(); + const bgFunc = (chalk as any)[bgColor]; + const display = bgFunc ? bgFunc(` ${bgColorName} `) : bgColorName; + return {display}; + } + })()} + - (b) cycle, (c) clear + + + + Press ESC to go back + + + + + Note: These settings are applied during rendering and don't add items to your widget list. + + + • Inherit colors: Default separators will use colors from the preceding widget + + + • Global Bold: Makes all text bold regardless of individual settings + + + • Override BG: All items will use this background color instead of their configured colors + + + + )} + + ); +}; + const App: React.FC = () => { const { exit } = useApp(); const [settings, setSettings] = useState(null); const [originalSettings, setOriginalSettings] = useState(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>({}); const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise } | 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 = () => { - {screen === 'main' && } + {screen === 'main' && ( + { + // Only persist menu selection if not exiting + if (value !== 'save' && value !== 'exit') { + const menuMap: Record = { + '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' && ( 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 && ( 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' && ( + { + setSettings(updatedSettings); + }} + onBack={() => { + // Save that we came from 'globalOptions' menu (index 3) + setMenuSelections({ ...menuSelections, main: 3 }); + setScreen('main'); + }} /> )} {screen === 'confirm' && confirmDialog && (