@owloops/claude-powerline
Version:
Beautiful vim-style powerline statusline for Claude Code with real-time usage tracking, git integration, and custom themes
207 lines (183 loc) • 5.85 kB
text/typescript
import type { PowerlineConfig } from "../config/loader";
import type { TuiData, BoxChars, LayoutMode, RenderCtx } from "./types";
import { SYMBOLS, TEXT_SYMBOLS, BOX_PRESETS } from "../utils/constants";
import { contentRow, bottomBorder } from "./primitives";
import {
buildTitleBar,
buildContextLine,
buildContextBar,
buildBlockBar,
buildWeeklyBar,
resolveSegments,
composeTemplate,
resolveTitleToken,
} from "./sections";
import {
renderWideMetrics,
renderWideBottom,
renderMediumMetrics,
renderMediumBottom,
renderNarrowMetrics,
renderNarrowBottom,
} from "./layouts";
import { renderGrid } from "./grid";
// Synchronized Output (DEC mode 2026): prevents tearing on multi-line renders.
// Terminals that don't support it silently ignore these sequences.
const SYNC_START = "\x1b[?2026h";
const SYNC_END = "\x1b[?2026l";
// No-op ANSI reset prepended to each line to prevent leading whitespace stripping.
// Claude Code's status line renderer strips leading spaces, but ANSI sequences at the
// start of a line protect subsequent whitespace from being trimmed.
const WS_GUARD = "\x1b[0m";
const MIN_PANEL_WIDTH = 32;
const WIDE_THRESHOLD = 80;
const MEDIUM_THRESHOLD = 55;
function getLayoutMode(panelWidth: number): LayoutMode {
if (panelWidth >= WIDE_THRESHOLD) {
return "wide";
}
if (panelWidth >= MEDIUM_THRESHOLD) {
return "medium";
}
return "narrow";
}
function calculatePanelWidth(terminalWidth: number | null): number {
if (terminalWidth && terminalWidth > 0) {
return Math.max(MIN_PANEL_WIDTH, terminalWidth);
}
return 80;
}
export interface TuiPanelOptions {
rawTerminalWidth?: number | null;
}
export async function renderTuiPanel(
data: TuiData,
box: BoxChars,
reset: string,
terminalWidth: number | null,
config: PowerlineConfig,
options?: TuiPanelOptions,
): Promise<string> {
const sym =
(config.display.charset || "unicode") === "text" ? TEXT_SYMBOLS : SYMBOLS;
const colors = data.colors;
// Grid path: when display.tui grid config is present
if (config.display.tui) {
const gridConfig = config.display.tui;
const rawWidth =
gridConfig.terminalWidth ?? options?.rawTerminalWidth ?? 120;
// Merge box character overrides with charset defaults
// Resolve box preset name or merge partial overrides with charset defaults
let mergedBox: BoxChars;
if (typeof gridConfig.box === "string") {
mergedBox = BOX_PRESETS[gridConfig.box] ?? box;
} else {
mergedBox = gridConfig.box ? { ...box, ...gridConfig.box } : box;
}
// Estimate content width for initial segment resolution (grid will compute final widths)
const estPanelWidth = Math.max(
gridConfig.minWidth ?? MIN_PANEL_WIDTH,
rawWidth - (gridConfig.widthReserve ?? 45),
);
const estInnerWidth = estPanelWidth - 2;
const estContentWidth = estInnerWidth - 2;
const ctx: RenderCtx = {
lines: [],
data,
box: mergedBox,
contentWidth: estContentWidth,
innerWidth: estInnerWidth,
sym,
config,
reset,
colors,
};
const resolved = resolveSegments(data, ctx);
const resolvedData = resolved.data;
const templates = resolved.templates;
const pf = colors.partFg;
const lateResolve = (
segment: string,
cellWidth: number,
): string | undefined => {
if (segment === "context") {
return buildContextLine(data, cellWidth, sym, reset, colors) ?? "";
}
if (segment === "context.bar") {
return buildContextBar(data, cellWidth, sym, reset, colors, pf);
}
if (segment === "block.bar") {
return buildBlockBar(data, cellWidth, sym, reset, colors, config, pf);
}
if (segment === "weekly.bar") {
return buildWeeklyBar(data, cellWidth, sym, reset, colors, pf);
}
const tmpl = templates[segment];
if (tmpl) {
return composeTemplate(tmpl.items, tmpl.gap, tmpl.justify, cellWidth);
}
return undefined;
};
const gridResult = renderGrid(
gridConfig,
resolvedData,
mergedBox,
rawWidth,
lateResolve,
);
const innerWidth = gridResult.panelWidth - 2;
const footerLeft = gridConfig.footer?.left
? resolveTitleToken(gridConfig.footer.left, data, resolvedData)
: undefined;
const footerRight = gridConfig.footer?.right
? resolveTitleToken(gridConfig.footer.right, data, resolvedData)
: undefined;
const lines: string[] = [];
lines.push(
buildTitleBar(
data,
mergedBox,
innerWidth,
gridConfig.title,
resolvedData,
),
);
lines.push(...gridResult.lines);
lines.push(bottomBorder(mergedBox, innerWidth, footerLeft, footerRight));
return SYNC_START + lines.map((l) => WS_GUARD + l).join("\n") + SYNC_END;
}
// Hardcoded path: existing layout system
const panelWidth = calculatePanelWidth(terminalWidth);
const innerWidth = panelWidth - 2;
const contentWidth = innerWidth - 2;
const mode = getLayoutMode(panelWidth);
const lines: string[] = [];
lines.push(buildTitleBar(data, box, innerWidth));
const contextLine = buildContextLine(data, contentWidth, sym, reset, colors);
if (contextLine) {
lines.push(contentRow(box, contextLine, innerWidth));
}
const ctx: RenderCtx = {
lines,
data,
box,
contentWidth,
innerWidth,
sym,
config,
reset,
colors,
};
if (mode === "wide") {
renderWideMetrics(ctx);
renderWideBottom(ctx);
} else if (mode === "medium") {
renderMediumMetrics(ctx);
renderMediumBottom(ctx);
} else {
renderNarrowMetrics(ctx);
renderNarrowBottom(ctx);
}
lines.push(bottomBorder(box, innerWidth));
return SYNC_START + lines.map((l) => WS_GUARD + l).join("\n") + SYNC_END;
}