diff --git a/README.md b/README.md index 3460019..943398b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ ## 🆕 Recent Updates +### v2.0.8 - Powerline Auto-Alignment + +![Powerline Auto-Alignment](https://raw.githubusercontent.com/sirmalloc/ccstatusline/main/screenshots/autoAlign.png) + +- **🎯 Widget Alignment** - Auto-align widgets across multiple status lines in Powerline mode for a clean, columnar layout (toggle with 'a' in Powerline Setup) + ### v2.0.7 - Current Working Directory & Session Cost ![Current Working Directory and Session Cost](https://raw.githubusercontent.com/sirmalloc/ccstatusline/main/screenshots/cwdAndSessionCost.png) diff --git a/package.json b/package.json index ece0603..f89a9ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccstatusline", - "version": "2.0.7", + "version": "2.0.8", "description": "A customizable status line formatter for Claude Code CLI", "module": "src/ccstatusline.ts", "type": "module", diff --git a/screenshots/autoAlign.png b/screenshots/autoAlign.png new file mode 100644 index 0000000..8b230bf Binary files /dev/null and b/screenshots/autoAlign.png differ diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 7e09139..831fe82 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -93,13 +93,18 @@ async function renderMultipleLines(data: StatusJSON) { isPreview: false }; + // Check if powerline mode is enabled and autoAlign is on + const isPowerlineMode = settings.powerline.enabled; + const autoAlign = settings.powerline.autoAlign; + const allLinesWidgets = (isPowerlineMode && autoAlign) ? lines : undefined; + // Render each line let globalSeparatorIndex = 0; for (let i = 0; i < lines.length; i++) { const lineItems = lines[i]; if (lineItems && lineItems.length > 0) { const lineContext = { ...context, lineIndex: i, globalSeparatorIndex }; - const line = renderStatusLine(lineItems, settings, lineContext); + const line = renderStatusLine(lineItems, settings, lineContext, allLinesWidgets); // Only output the line if it has content (not just ANSI codes) // Strip ANSI codes to check if there's actual text diff --git a/src/tui/components/PowerlineSetup.tsx b/src/tui/components/PowerlineSetup.tsx index 8571573..24e56c3 100644 --- a/src/tui/components/PowerlineSetup.tsx +++ b/src/tui/components/PowerlineSetup.tsx @@ -183,6 +183,10 @@ export const PowerlineSetup: React.FC = ({ } else if (input === 'i' || input === 'I') { // Show font installation consent prompt setConfirmingFontInstall(true); + } else if ((input === 'a' || input === 'A') && powerlineConfig.enabled) { + // Toggle autoAlign when powerline is enabled + const newConfig = { ...powerlineConfig, autoAlign: !powerlineConfig.autoAlign }; + onUpdate({ ...settings, powerline: newConfig }); } } }); @@ -367,7 +371,7 @@ export const PowerlineSetup: React.FC = ({ <> - {' Font Status: '} + {' Font Status: '} {powerlineFontStatus.installed ? ( <> ✓ Installed @@ -383,7 +387,7 @@ export const PowerlineSetup: React.FC = ({ - Powerline Mode: + Powerline Mode: {powerlineConfig.enabled ? '✓ Enabled ' : '✗ Disabled '} @@ -391,11 +395,21 @@ export const PowerlineSetup: React.FC = ({ {powerlineConfig.enabled && ( - - - When enabled, global overrides are disabled and powerline separators are used - - + <> + + Align Widgets: + + {powerlineConfig.autoAlign ? '✓ Enabled ' : '✗ Disabled '} + + - Press (a) to toggle + + + + + When enabled, global overrides are disabled and powerline separators are used + + + )} diff --git a/src/tui/components/StatusLinePreview.tsx b/src/tui/components/StatusLinePreview.tsx index d0b6366..bef731e 100644 --- a/src/tui/components/StatusLinePreview.tsx +++ b/src/tui/components/StatusLinePreview.tsx @@ -27,7 +27,8 @@ const renderSingleLine = ( widthDetectionAvailable: boolean, settings: Settings, lineIndex: number, - globalSeparatorIndex: number + globalSeparatorIndex: number, + allLinesWidgets?: WidgetItem[][] ): RenderResult => { // Create render context for preview const context: RenderContext = { @@ -37,7 +38,7 @@ const renderSingleLine = ( globalSeparatorIndex }; - return renderStatusLineWithInfo(widgets, settings, context); + return renderStatusLineWithInfo(widgets, settings, context, allLinesWidgets); }; export const StatusLinePreview: React.FC = ({ lines, terminalWidth, settings, onTruncationChange }) => { @@ -49,6 +50,11 @@ export const StatusLinePreview: React.FC = ({ lines, ter if (!settings) return { renderedLines: [], anyTruncated: false }; + // Check if powerline mode is enabled and autoAlign is on + const isPowerlineMode = settings.powerline.enabled; + const autoAlign = settings.powerline.autoAlign; + const allLinesWidgets = (isPowerlineMode && autoAlign) ? lines : undefined; + let globalSeparatorIndex = 0; const result: string[] = []; let truncated = false; @@ -56,7 +62,7 @@ export const StatusLinePreview: React.FC = ({ lines, ter for (let i = 0; i < lines.length; i++) { const lineItems = lines[i]; if (lineItems && lineItems.length > 0) { - const renderResult = renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings, i, globalSeparatorIndex); + const renderResult = renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings, i, globalSeparatorIndex, allLinesWidgets); result.push(renderResult.line); if (renderResult.wasTruncated) { truncated = true; diff --git a/src/types/PowerlineConfig.ts b/src/types/PowerlineConfig.ts index 34883dd..d089b01 100644 --- a/src/types/PowerlineConfig.ts +++ b/src/types/PowerlineConfig.ts @@ -7,7 +7,8 @@ export const PowerlineConfigSchema = z.object({ separatorInvertBackground: z.array(z.boolean()).default([false]), startCaps: z.array(z.string()).default([]), endCaps: z.array(z.string()).default([]), - theme: z.string().optional() + theme: z.string().optional(), + autoAlign: z.boolean().default(false) }); // Inferred type from schema diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 05e2d45..889143e 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -55,7 +55,8 @@ export const SettingsSchema = z.object({ separatorInvertBackground: [false], startCaps: [], endCaps: [], - theme: undefined + theme: undefined, + autoAlign: false }), updatemessage: z.object({ message: z.string().nullable().optional(), diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 92dc48b..7336068 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -45,7 +45,8 @@ function renderPowerlineStatusLine( settings: Settings, context: RenderContext, lineIndex = 0, // Which line we're rendering (for theme color cycling) - globalSeparatorOffset = 0 // Starting separator index for this line + globalSeparatorOffset = 0, // Starting separator index for this line + allLinesWidgets?: WidgetItem[][] // All lines widgets for alignment calculation ): string { const powerlineConfig = settings.powerline as Record | undefined; const config = powerlineConfig ?? {}; @@ -209,6 +210,72 @@ function renderPowerlineStatusLine( if (widgetElements.length === 0) return ''; + // Apply auto-alignment if enabled + const autoAlign = config.autoAlign as boolean | undefined; + if (autoAlign && allLinesWidgets) { + // Calculate max width for each widget position across all lines + const maxWidths: number[] = []; + + // First, collect all widget contents for each line + const allLinesElements: { content: string; bgColor?: string; fgColor?: string; widget: WidgetItem }[][] = []; + + for (const lineWidgets of allLinesWidgets) { + const filteredLineWidgets = lineWidgets.filter(w => w.type !== 'separator' && w.type !== 'flex-separator'); + const lineElements: { content: string; bgColor?: string; fgColor?: string; widget: WidgetItem }[] = []; + + for (const widget of filteredLineWidgets) { + let widgetText = ''; + + try { + const widgetImpl = getWidget(widget.type); + widgetText = widgetImpl.render(widget, context, settings) ?? ''; + } catch { + continue; + } + + if (widgetText) { + const padding = settings.defaultPadding ?? ''; + const paddedText = `${padding}${widgetText}${padding}`; + lineElements.push({ + content: paddedText, + bgColor: widget.backgroundColor, + fgColor: widget.color, + widget: widget + }); + } + } + allLinesElements.push(lineElements); + } + + // Calculate max width for each position + for (let pos = 0; pos < Math.max(...allLinesElements.map(line => line.length)); pos++) { + let maxWidth = 0; + for (const lineElements of allLinesElements) { + const element = lineElements[pos]; + if (element) { + // Get the plain text length (without ANSI codes) + const plainLength = element.content.replace(ANSI_REGEX, '').length; + maxWidth = Math.max(maxWidth, plainLength); + } + } + maxWidths.push(maxWidth); + } + + // Apply padding to current line's widgets + for (let i = 0; i < widgetElements.length; i++) { + const element = widgetElements[i]; + const maxWidth = maxWidths[i]; + if (element && maxWidth !== undefined) { + const currentLength = element.content.replace(ANSI_REGEX, '').length; + const paddingNeeded = maxWidth - currentLength; + if (paddingNeeded > 0) { + // Add spaces to the right of the content + element.content += ' '.repeat(paddingNeeded); + } + } + } + } + // Build the final powerline string let result = ''; @@ -444,9 +511,10 @@ export interface RenderResult { export function renderStatusLineWithInfo( widgets: WidgetItem[], settings: Settings, - context: RenderContext + context: RenderContext, + allLinesWidgets?: WidgetItem[][] ): RenderResult { - const line = renderStatusLine(widgets, settings, context); + const line = renderStatusLine(widgets, settings, context, allLinesWidgets); // Check if line contains the truncation ellipsis const wasTruncated = line.includes('...'); return { line, wasTruncated }; @@ -455,7 +523,8 @@ export function renderStatusLineWithInfo( export function renderStatusLine( widgets: WidgetItem[], settings: Settings, - context: RenderContext + context: RenderContext, + allLinesWidgets?: WidgetItem[][] ): string { // Force 24-bit color for non-preview statusline rendering // Chalk level is now set globally in ccstatusline.ts and tui.tsx @@ -469,9 +538,9 @@ export function renderStatusLine( const isPowerlineMode = Boolean(powerlineSettings?.enabled); // If powerline mode is enabled, use powerline renderer - if (isPowerlineMode) { - return renderPowerlineStatusLine(widgets, settings, context, context.lineIndex ?? 0, context.globalSeparatorIndex ?? 0); - } + if (isPowerlineMode) + return renderPowerlineStatusLine(widgets, settings, context, context.lineIndex ?? 0, context.globalSeparatorIndex ?? 0, allLinesWidgets); + // Helper to apply colors with optional background and bold override const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => { // Override foreground color takes precedence over EVERYTHING, including passed foreground color