Files
pi-statusline/render.ts

117 lines
4.6 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");
const ANSI_PREFIX_RE = /^\u001b\[[0-9;]*m/;
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") {
const match = text.slice(index).match(ANSI_PREFIX_RE);
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} `);
}
// ANSI-Aware Slice - Extract a substring by visible character positions,
// preserving ANSI escape codes that precede visible characters.
function ansiSlice(text: string, start: number, end: number): string {
let output = "";
let visible = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === "\x1b") {
const match = text.slice(i).match(ANSI_PREFIX_RE);
if (match) {
if (visible >= start && visible < end) output += match[0];
i += match[0].length - 1;
continue;
}
}
if (visible >= start && visible < end) output += text[i];
visible++;
if (visible >= end) break;
}
return output;
}
export function overlay(base: string, text: string): string {
const baseWidth = visibleWidth(base);
const textWidth = visibleWidth(text);
if (textWidth >= baseWidth) return truncate(text, baseWidth);
const start = Math.max(0, Math.floor((baseWidth - textWidth) / 2));
return `${ansiSlice(base, 0, start)}${text}${ansiSlice(base, start + textWidth, baseWidth)}`;
}
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;
}