Fixes for inefficient rendering in auto-alignment mode

This commit is contained in:
Matthew Breedlove
2025-08-24 02:45:38 -04:00
parent 174dba2832
commit ad85f951a4
4 changed files with 148 additions and 82 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "2.0.8",
"version": "2.0.9",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
+8 -6
View File
@@ -18,6 +18,8 @@ import {
getTokenMetrics
} from './utils/jsonl';
import {
calculateMaxWidthsFromPreRendered,
preRenderAllWidgets,
renderStatusLine,
type RenderContext,
type StatusJSON
@@ -93,18 +95,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;
// Always pre-render all widgets once (for efficiency)
const preRenderedLines = preRenderAllWidgets(lines, settings, context);
const preCalculatedMaxWidths = calculateMaxWidthsFromPreRendered(preRenderedLines, settings);
// Render each line
// Render each line using pre-rendered content
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, allLinesWidgets);
const preRenderedWidgets = preRenderedLines[i] ?? [];
const line = renderStatusLine(lineItems, settings, lineContext, preRenderedWidgets, preCalculatedMaxWidths);
// Only output the line if it has content (not just ANSI codes)
// Strip ANSI codes to check if there's actual text
+11 -7
View File
@@ -8,7 +8,10 @@ import React from 'react';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
calculateMaxWidthsFromPreRendered,
preRenderAllWidgets,
renderStatusLineWithInfo,
type PreRenderedWidget,
type RenderContext,
type RenderResult
} from '../../utils/renderer';
@@ -28,7 +31,8 @@ const renderSingleLine = (
settings: Settings,
lineIndex: number,
globalSeparatorIndex: number,
allLinesWidgets?: WidgetItem[][]
preRenderedWidgets: PreRenderedWidget[],
preCalculatedMaxWidths: number[]
): RenderResult => {
// Create render context for preview
const context: RenderContext = {
@@ -38,7 +42,7 @@ const renderSingleLine = (
globalSeparatorIndex
};
return renderStatusLineWithInfo(widgets, settings, context, allLinesWidgets);
return renderStatusLineWithInfo(widgets, settings, context, preRenderedWidgets, preCalculatedMaxWidths);
};
export const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ lines, terminalWidth, settings, onTruncationChange }) => {
@@ -50,10 +54,9 @@ export const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ 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;
// Always pre-render all widgets once (for efficiency)
const preRenderedLines = preRenderAllWidgets(lines, settings, { terminalWidth, isPreview: true });
const preCalculatedMaxWidths = calculateMaxWidthsFromPreRendered(preRenderedLines, settings);
let globalSeparatorIndex = 0;
const result: string[] = [];
@@ -62,7 +65,8 @@ export const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ 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, allLinesWidgets);
const preRenderedWidgets = preRenderedLines[i] ?? [];
const renderResult = renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings, i, globalSeparatorIndex, preRenderedWidgets, preCalculatedMaxWidths);
result.push(renderResult.line);
if (renderResult.wasTruncated) {
truncated = true;
+128 -68
View File
@@ -46,7 +46,8 @@ function renderPowerlineStatusLine(
context: RenderContext,
lineIndex = 0, // Which line we're rendering (for theme color cycling)
globalSeparatorOffset = 0, // Starting separator index for this line
allLinesWidgets?: WidgetItem[][] // All lines widgets for alignment calculation
preRenderedWidgets: PreRenderedWidget[], // Pre-rendered widgets for this line
preCalculatedMaxWidths: number[] // Pre-calculated max widths for alignment
): string {
const powerlineConfig = settings.powerline as Record<string, unknown> | undefined;
const config = powerlineConfig ?? {};
@@ -140,14 +141,17 @@ function renderPowerlineStatusLine(
continue;
}
// Use widget registry for regular widgets
try {
const widgetImpl = getWidget(widget.type);
widgetText = widgetImpl.render(widget, context, settings) ?? '';
defaultColor = widgetImpl.getDefaultColor();
} catch {
// Unknown widget type - skip
continue;
// Use pre-rendered content
const preRendered = preRenderedWidgets[i];
if (preRendered?.content) {
widgetText = preRendered.content;
// Get default color from widget impl for consistency
try {
const widgetImpl = getWidget(widget.type);
defaultColor = widgetImpl.getDefaultColor();
} catch {
// Keep default
}
}
if (widgetText) {
@@ -212,59 +216,11 @@ function renderPowerlineStatusLine(
// 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
if (autoAlign) {
// Apply padding to current line's widgets based on pre-calculated max widths
for (let i = 0; i < widgetElements.length; i++) {
const element = widgetElements[i];
const maxWidth = maxWidths[i];
const maxWidth = preCalculatedMaxWidths[i];
if (element && maxWidth !== undefined) {
const currentLength = element.content.replace(ANSI_REGEX, '').length;
const paddingNeeded = maxWidth - currentLength;
@@ -508,13 +464,105 @@ export interface RenderResult {
wasTruncated: boolean;
}
export interface PreRenderedWidget {
content: string; // The rendered widget text (without padding)
plainLength: number; // Length without ANSI codes
widget: WidgetItem; // Original widget config
}
// Pre-render all widgets once and cache the results
export function preRenderAllWidgets(
allLinesWidgets: WidgetItem[][],
settings: Settings,
context: RenderContext
): PreRenderedWidget[][] {
const preRenderedLines: PreRenderedWidget[][] = [];
// Process each line
for (const lineWidgets of allLinesWidgets) {
const preRenderedLine: PreRenderedWidget[] = [];
for (const widget of lineWidgets) {
// Skip separators as they're handled differently
if (widget.type === 'separator' || widget.type === 'flex-separator') {
preRenderedLine.push({
content: '', // Separators are handled specially
plainLength: 0,
widget
});
continue;
}
let widgetText = '';
try {
const widgetImpl = getWidget(widget.type);
widgetText = widgetImpl.render(widget, context, settings) ?? '';
} catch {
// Unknown widget type - store empty content
preRenderedLine.push({
content: '',
plainLength: 0,
widget
});
continue;
}
// Store the rendered content without padding (padding is applied later)
const plainLength = widgetText.replace(ANSI_REGEX, '').length;
preRenderedLine.push({
content: widgetText,
plainLength,
widget
});
}
preRenderedLines.push(preRenderedLine);
}
return preRenderedLines;
}
// Calculate max widths from pre-rendered widgets for alignment
export function calculateMaxWidthsFromPreRendered(
preRenderedLines: PreRenderedWidget[][],
settings: Settings
): number[] {
const maxWidths: number[] = [];
const defaultPadding = settings.defaultPadding ?? '';
const paddingLength = defaultPadding.length;
for (const preRenderedLine of preRenderedLines) {
const filteredWidgets = preRenderedLine.filter(
w => w.widget.type !== 'separator' && w.widget.type !== 'flex-separator' && w.content
);
for (let pos = 0; pos < filteredWidgets.length; pos++) {
const widget = filteredWidgets[pos];
if (!widget)
continue;
// Width includes padding on both sides
const totalWidth = widget.plainLength + (paddingLength * 2);
const currentMax = maxWidths[pos];
if (currentMax === undefined) {
maxWidths[pos] = totalWidth;
} else {
maxWidths[pos] = Math.max(currentMax, totalWidth);
}
}
}
return maxWidths;
}
export function renderStatusLineWithInfo(
widgets: WidgetItem[],
settings: Settings,
context: RenderContext,
allLinesWidgets?: WidgetItem[][]
preRenderedWidgets: PreRenderedWidget[],
preCalculatedMaxWidths: number[]
): RenderResult {
const line = renderStatusLine(widgets, settings, context, allLinesWidgets);
const line = renderStatusLine(widgets, settings, context, preRenderedWidgets, preCalculatedMaxWidths);
// Check if line contains the truncation ellipsis
const wasTruncated = line.includes('...');
return { line, wasTruncated };
@@ -524,7 +572,8 @@ export function renderStatusLine(
widgets: WidgetItem[],
settings: Settings,
context: RenderContext,
allLinesWidgets?: WidgetItem[][]
preRenderedWidgets: PreRenderedWidget[],
preCalculatedMaxWidths: number[]
): string {
// Force 24-bit color for non-preview statusline rendering
// Chalk level is now set globally in ccstatusline.ts and tui.tsx
@@ -539,7 +588,7 @@ export function renderStatusLine(
// If powerline mode is enabled, use powerline renderer
if (isPowerlineMode)
return renderPowerlineStatusLine(widgets, settings, context, context.lineIndex ?? 0, context.globalSeparatorIndex ?? 0, allLinesWidgets);
return renderPowerlineStatusLine(widgets, settings, context, context.lineIndex ?? 0, context.globalSeparatorIndex ?? 0, preRenderedWidgets, preCalculatedMaxWidths);
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
@@ -652,12 +701,23 @@ export function renderStatusLine(
// Use widget registry for regular widgets
try {
const widgetImpl = getWidget(widget.type);
const widgetText = widgetImpl.render(widget, context, settings);
let widgetText: string | undefined;
let defaultColor = 'white';
// Use pre-rendered content
const preRendered = preRenderedWidgets[i];
if (preRendered?.content) {
widgetText = preRendered.content;
// Get default color from widget impl for consistency
try {
const widgetImpl = getWidget(widget.type);
defaultColor = widgetImpl.getDefaultColor();
} catch {
// Keep default
}
}
if (widgetText) {
const defaultColor = widgetImpl.getDefaultColor();
// Special handling for custom-command with preserveColors
if (widget.type === 'custom-command' && widget.preserveColors) {
// Handle max width truncation for commands with ANSI codes