122 lines
2.8 KiB
TypeScript
122 lines
2.8 KiB
TypeScript
/**
|
|
* Formats multi-line dialog messages for readable terminal display.
|
|
*
|
|
* Ink's `wrap="wrap"` breaks long lines mid-word, which looks broken for
|
|
* dot-separated template validation paths. We pre-split on newlines and break
|
|
* long lines at `.` segment boundaries instead.
|
|
*/
|
|
|
|
/**
|
|
* Hard-wraps text when a single segment still exceeds the maximum width.
|
|
*/
|
|
function hardWrapLine(line: string, maxWidth: number): string[] {
|
|
if (line.length <= maxWidth) {
|
|
return [line];
|
|
}
|
|
|
|
const wrapped: string[] = [];
|
|
let remaining = line;
|
|
|
|
while (remaining.length > maxWidth) {
|
|
wrapped.push(remaining.slice(0, maxWidth));
|
|
remaining = ` ${remaining.slice(maxWidth)}`;
|
|
}
|
|
|
|
if (remaining.length > 0) {
|
|
wrapped.push(remaining);
|
|
}
|
|
|
|
return wrapped;
|
|
}
|
|
|
|
/**
|
|
* Breaks a long line at dot-separated segments, indenting continuations.
|
|
*/
|
|
function breakLongLineAtDots(line: string, maxWidth: number): string[] {
|
|
const segments: string[] = [];
|
|
let segmentStart = 0;
|
|
|
|
for (let index = 0; index < line.length; index += 1) {
|
|
if (line[index] === "." && index > 0) {
|
|
segments.push(line.slice(segmentStart, index + 1));
|
|
segmentStart = index + 1;
|
|
}
|
|
}
|
|
|
|
if (segmentStart < line.length) {
|
|
segments.push(line.slice(segmentStart));
|
|
}
|
|
|
|
if (segments.length === 0) {
|
|
return hardWrapLine(line, maxWidth);
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
let current = "";
|
|
|
|
for (const segment of segments) {
|
|
const candidate = current + segment;
|
|
|
|
if (candidate.length > maxWidth && current.length > 0) {
|
|
lines.push(current);
|
|
current = ` ${segment}`;
|
|
continue;
|
|
}
|
|
|
|
if (candidate.length > maxWidth) {
|
|
lines.push(...hardWrapLine(segment, maxWidth));
|
|
current = "";
|
|
continue;
|
|
}
|
|
|
|
current = candidate;
|
|
}
|
|
|
|
if (current.length > 0) {
|
|
lines.push(current);
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Splits a dialog message into display lines that fit the available width.
|
|
*/
|
|
export function formatDialogMessageLines(
|
|
message: string,
|
|
contentWidth: number,
|
|
): string[] {
|
|
const output: string[] = [];
|
|
|
|
for (const rawLine of message.split("\n")) {
|
|
const line = rawLine.trimEnd();
|
|
if (line.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (line.length <= contentWidth) {
|
|
output.push(line);
|
|
continue;
|
|
}
|
|
|
|
output.push(...breakLongLineAtDots(line, contentWidth));
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Computes dialog width from the terminal size.
|
|
*/
|
|
export function getMessageDialogWidth(terminalColumns: number): number {
|
|
return Math.min(Math.max(terminalColumns - 4, 60), 100);
|
|
}
|
|
|
|
/** Inner text width after dialog border and horizontal padding. */
|
|
export function getMessageContentWidth(dialogWidth: number): number {
|
|
return Math.max(dialogWidth - 6, 40);
|
|
}
|
|
|
|
/** Maximum number of body lines shown before truncating with a summary. */
|
|
export const MAX_MESSAGE_DIALOG_LINES = 24;
|