Files
pi-statusline/render.ts

94 lines
3.9 KiB
TypeScript

import type { ModuleSpec, RenderedModule } from "./types";
// eslint-disable-next-line no-control-regex
const ANSI_RE = new RegExp("\\u001b\\[[0-9;]*m", "g");
export function visibleWidth(text: string): number {
return text.replace(ANSI_RE, "").length;
}
export function truncate(text: string, width: number): string {
if (width <= 0) return "";
if (visibleWidth(text) <= width) return text;
// ANSI-Aware Truncation
let output = "";
let count = 0;
for (let index = 0; index < text.length && count < width; index++) {
if (text[index] === "\x1b") {
// eslint-disable-next-line no-control-regex
const match = text.slice(index).match(new RegExp("^\\u001b\\[[0-9;]*m"));
if (match) {
output += match[0];
index += match[0].length - 1;
continue;
}
}
output += text[index];
count++;
}
return `${output}\x1b[0m`;
}
export function joinText(parts: string[], separator: string): string {
return parts.filter(Boolean).join(` ${separator} `);
}
export function overlay(base: string, text: string): string {
if (visibleWidth(text) >= visibleWidth(base)) return truncate(text, visibleWidth(base));
const start = Math.max(0, Math.floor((visibleWidth(base) - visibleWidth(text)) / 2));
return `${base.slice(0, start)}${text}${base.slice(start + visibleWidth(text))}`;
}
function dimText(text: string, theme?: any): string {
return text && theme?.fg ? theme.fg("dim", text) : text;
}
function renderGroup(modules: RenderedModule[], width: number, separator: string, theme?: any): string {
const fixed = modules.map(module => module.text ?? "");
const growModules = modules.filter(module => module.render);
const groupSeparator = modules.every(module => module.render) ? " " : ` ${separator} `;
const renderedSeparator = modules.every(module => module.render) ? " " : dimText(groupSeparator, theme);
const fixedText = fixed.filter(Boolean).map(text => dimText(text, theme)).join(dimText(` ${separator} `, theme));
if (growModules.length === 0) return truncate(fixedText, width);
const separatorWidth = Math.max(0, modules.length - 1) * groupSeparator.length;
const fixedWidth = fixed.reduce((total, text) => total + visibleWidth(text), 0);
const remaining = Math.max(0, width - fixedWidth - separatorWidth);
const totalGrow = growModules.reduce((total, module) => total + (module.grow ?? 1), 0) || 1;
let used = 0;
const rendered = modules.map(module => {
if (!module.render) return dimText(module.text ?? "", theme);
const isLastGrow = growModules[growModules.length - 1] === module;
const allocated = isLastGrow ? remaining - used : Math.floor(remaining * ((module.grow ?? 1) / totalGrow));
used += allocated;
return module.render(Math.max(0, allocated), theme);
});
return truncate(rendered.filter(Boolean).join(renderedSeparator), width);
}
export function renderRow(rendered: Record<"left" | "center" | "right", RenderedModule[]>, width: number, separator: string, theme?: any): string {
const leftText = renderGroup(rendered.left, width, separator, theme);
const rightText = renderGroup(rendered.right, width, separator, theme);
const gap = leftText && rightText ? 1 : 0;
const centerWidth = Math.max(0, width - visibleWidth(leftText) - visibleWidth(rightText) - gap);
const centerText = renderGroup(rendered.center, centerWidth, separator, theme);
if (centerText) {
const leftPad = leftText ? `${leftText} ` : "";
const rightPad = rightText ? ` ${rightText}` : "";
return truncate(`${leftPad}${centerText}${rightPad}`, width);
}
if (!leftText) return `${" ".repeat(Math.max(0, width - visibleWidth(rightText)))}${truncate(rightText, width)}`;
if (!rightText) return truncate(leftText, width);
const padding = " ".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(rightText)));
return truncate(`${leftText}${padding}${rightText}`, width);
}
export function moduleType(spec: ModuleSpec): string {
return typeof spec === "string" ? spec : spec.type;
}