Terminal: refine table wrapping and width handling

This commit is contained in:
Vincent Koc
2026-03-11 01:39:43 -04:00
parent f7f75519ad
commit c58fffdab6
2 changed files with 54 additions and 4 deletions

View File

@@ -83,6 +83,38 @@ describe("renderTable", () => {
}
});
it("trims leading spaces on wrapped ANSI-colored continuation lines", () => {
const out = renderTable({
width: 113,
columns: [
{ key: "Status", header: "Status", minWidth: 10 },
{ key: "Skill", header: "Skill", minWidth: 18, flex: true },
{ key: "Description", header: "Description", minWidth: 24, flex: true },
{ key: "Source", header: "Source", minWidth: 10 },
],
rows: [
{
Status: "✓ ready",
Skill: "🌤️ weather",
Description:
`\x1b[2mGet current weather and forecasts via wttr.in or Open-Meteo. ` +
`Use when: user asks about weather, temperature, or forecasts for any location.` +
`\x1b[0m`,
Source: "openclaw-bundled",
},
],
});
const lines = out
.trimEnd()
.split("\n")
.filter((line) => line.includes("Use when"));
expect(lines).toHaveLength(1);
expect(lines[0]).toContain("\u001b[2mUse when");
expect(lines[0]).not.toContain("│ Use when");
expect(lines[0]).not.toContain("│ \x1b[2m Use when");
});
it("respects explicit newlines in cell values", () => {
const out = renderTable({
width: 48,

View File

@@ -151,6 +151,20 @@ function wrapLine(text: string, width: number): string[] {
lines.push(cleaned);
};
const trimLeadingSpaces = (tokens: Token[]) => {
while (true) {
const firstCharIndex = tokens.findIndex((token) => token.kind === "char");
if (firstCharIndex < 0) {
return;
}
const firstChar = tokens[firstCharIndex];
if (!firstChar || !isSpaceChar(firstChar.value)) {
return;
}
tokens.splice(firstCharIndex, 1);
}
};
const flushAt = (breakAt: number | null) => {
if (buf.length === 0) {
return;
@@ -166,10 +180,7 @@ function wrapLine(text: string, width: number): string[] {
const left = buf.slice(0, breakAt);
const rest = buf.slice(breakAt);
pushLine(bufToString(left));
while (rest.length > 0 && rest[0]?.kind === "char" && isSpaceChar(rest[0].value)) {
rest.shift();
}
trimLeadingSpaces(rest);
buf.length = 0;
buf.push(...rest);
@@ -201,6 +212,9 @@ function wrapLine(text: string, width: number): string[] {
if (bufVisible + charWidth > width && bufVisible > 0) {
flushAt(lastBreakIndex);
}
if (bufVisible === 0 && isSpaceChar(ch)) {
continue;
}
buf.push(token);
bufVisible += charWidth;
@@ -234,6 +248,10 @@ function normalizeWidth(n: number | undefined): number | undefined {
return Math.floor(n);
}
export function getTerminalTableWidth(minWidth = 60, fallbackWidth = 120): number {
return Math.max(minWidth, process.stdout.columns ?? fallbackWidth);
}
export function renderTable(opts: RenderTableOptions): string {
const rows = opts.rows.map((row) => {
const next: Record<string, string> = {};