This commit is contained in:
alex wiesner
2026-04-09 11:31:06 +01:00
parent 8fec8e28f4
commit 18245c778e
155 changed files with 16206 additions and 2980 deletions

View File

@@ -76,3 +76,59 @@ export function allQuestionsAnswered(questions, answers) {
export function nextTabAfterAnswer(currentTab, questionCount) {
return currentTab < questionCount - 1 ? currentTab + 1 : questionCount;
}
function takeWrappedSegment(text, maxWidth) {
if (text.length <= maxWidth) {
return { line: text, rest: "" };
}
let breakpoint = -1;
for (let index = 0; index < maxWidth; index += 1) {
if (/\s/.test(text[index])) {
breakpoint = index;
}
}
if (breakpoint > 0) {
return {
line: text.slice(0, breakpoint).trimEnd(),
rest: text.slice(breakpoint + 1).trimStart(),
};
}
return {
line: text.slice(0, maxWidth),
rest: text.slice(maxWidth),
};
}
export function wrapPrefixedText(text, width, firstPrefix = "", continuationPrefix = firstPrefix) {
const source = String(text ?? "");
if (source.length === 0) {
return [firstPrefix];
}
const lines = [];
const blocks = source.split(/\r?\n/);
let isFirstLine = true;
for (const block of blocks) {
let remaining = block.trim();
if (remaining.length === 0) {
lines.push(isFirstLine ? firstPrefix : continuationPrefix);
isFirstLine = false;
continue;
}
while (remaining.length > 0) {
const prefix = isFirstLine ? firstPrefix : continuationPrefix;
const maxTextWidth = Math.max(1, width - prefix.length);
const { line, rest } = takeWrappedSegment(remaining, maxTextWidth);
lines.push(prefix + line);
remaining = rest;
isFirstLine = false;
}
}
return lines;
}

View File

@@ -11,6 +11,7 @@ import {
nextTabAfterAnswer,
normalizeQuestions,
summarizeAnswers,
wrapPrefixedText,
} from "./question-core.mjs";
test("normalizeQuestions adds default labels and appends the Something else option", () => {
@@ -157,3 +158,23 @@ test("nextTabAfterAnswer advances through questions and then to the submit tab",
assert.equal(nextTabAfterAnswer(1, 3), 2);
assert.equal(nextTabAfterAnswer(2, 3), 3);
});
test("wrapPrefixedText wraps long prompts and keeps the prefix on continuation lines", () => {
assert.deepEqual(wrapPrefixedText("Pick the best rollout strategy for this change", 18, " "), [
" Pick the best",
" rollout strategy",
" for this change",
]);
});
test("wrapPrefixedText supports a different continuation prefix for wrapped option labels", () => {
assert.deepEqual(wrapPrefixedText("Very long option label", 16, "> 1. ", " "), [
"> 1. Very long",
" option",
" label",
]);
});
test("wrapPrefixedText breaks oversized words when there is no whitespace boundary", () => {
assert.deepEqual(wrapPrefixedText("supercalifragilistic", 8), ["supercal", "ifragili", "stic"]);
});

View File

@@ -11,6 +11,7 @@ import {
nextTabAfterAnswer,
normalizeQuestions,
summarizeAnswers,
wrapPrefixedText,
} from "./question-core.mjs";
interface QuestionOption {
@@ -220,6 +221,32 @@ async function runQuestionFlow(ctx: any, questions: Question[]): Promise<Questio
const question = currentQuestion();
const options = currentOptions();
function addWrapped(text: string, color: string, firstPrefix = "", continuationPrefix = firstPrefix) {
for (const line of wrapPrefixedText(text, width, firstPrefix, continuationPrefix)) {
add(theme.fg(color, line));
}
}
function addWrappedOption(option: QuestionOption, index: number, selected: boolean) {
const firstPrefix = `${selected ? "> " : " "}${index + 1}. `;
const continuationPrefix = " ".repeat(firstPrefix.length);
addWrapped(option.label, selected ? "accent" : "text", firstPrefix, continuationPrefix);
if (option.description) {
addWrapped(option.description, "muted", " ");
}
}
function addWrappedReviewAnswer(questionLabel: string, value: string) {
const firstPrefix = ` ${questionLabel}: `;
const continuationPrefix = " ".repeat(firstPrefix.length);
const wrapped = wrapPrefixedText(value, width, firstPrefix, continuationPrefix);
for (let index = 0; index < wrapped.length; index += 1) {
const prefix = index === 0 ? firstPrefix : continuationPrefix;
const line = wrapped[index]!;
add(theme.fg("muted", prefix) + theme.fg("text", line.slice(prefix.length)));
}
}
add(theme.fg("accent", "─".repeat(width)));
if (isMulti) {
@@ -247,19 +274,14 @@ async function runQuestionFlow(ctx: any, questions: Question[]): Promise<Questio
}
if (inputMode && question) {
add(theme.fg("text", ` ${question.prompt}`));
addWrapped(question.prompt, "text", " ");
lines.push("");
for (let index = 0; index < options.length; index += 1) {
const option = options[index]!;
const prefix = index === optionIndex ? theme.fg("accent", "> ") : " ";
add(prefix + theme.fg(index === optionIndex ? "accent" : "text", `${index + 1}. ${option.label}`));
if (option.description) {
add(` ${theme.fg("muted", option.description)}`);
}
addWrappedOption(options[index]!, index, index === optionIndex);
}
lines.push("");
add(theme.fg("muted", " Your answer:"));
for (const line of editor.render(width - 2)) {
for (const line of editor.render(Math.max(1, width - 2))) {
add(` ${line}`);
}
} else if (isMulti && currentTab === questions.length) {
@@ -269,7 +291,7 @@ async function runQuestionFlow(ctx: any, questions: Question[]): Promise<Questio
const answer = answers.get(reviewQuestion.id);
if (!answer) continue;
const label = answer.wasCustom ? `(wrote) ${answer.label}` : `${answer.index}. ${answer.label}`;
add(`${theme.fg("muted", ` ${reviewQuestion.label}: `)}${theme.fg("text", label)}`);
addWrappedReviewAnswer(reviewQuestion.label, label);
}
lines.push("");
if (allQuestionsAnswered(questions, answers)) {
@@ -278,15 +300,10 @@ async function runQuestionFlow(ctx: any, questions: Question[]): Promise<Questio
add(theme.fg("warning", " All questions must be answered before submit"));
}
} else if (question) {
add(theme.fg("text", ` ${question.prompt}`));
addWrapped(question.prompt, "text", " ");
lines.push("");
for (let index = 0; index < options.length; index += 1) {
const option = options[index]!;
const prefix = index === optionIndex ? theme.fg("accent", "> ") : " ";
add(prefix + theme.fg(index === optionIndex ? "accent" : "text", `${index + 1}. ${option.label}`));
if (option.description) {
add(` ${theme.fg("muted", option.description)}`);
}
addWrappedOption(options[index]!, index, index === optionIndex);
}
}