@xynogen/pix-ask 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/questionnaire.ts +75 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-ask",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Pi tool — structured questionnaire UI (ask_user)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@xynogen/pix-pretty": "^1.7.9",
|
|
39
|
-
"@xynogen/pix-runtime": "^0.
|
|
39
|
+
"@xynogen/pix-runtime": "^0.3.0",
|
|
40
40
|
"typebox": "^1.1.38"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
package/src/questionnaire.ts
CHANGED
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
truncateToWidth,
|
|
13
13
|
wrapTextWithAnsi,
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
frameModal,
|
|
17
|
+
MIN_MODAL_HEIGHT,
|
|
18
|
+
ModalPager,
|
|
19
|
+
modalWidth,
|
|
20
|
+
terminalModalHeight,
|
|
21
|
+
} from "@xynogen/pix-pretty/modal-frame";
|
|
16
22
|
import { dim } from "./components.js";
|
|
17
23
|
import { checkboxGlyphs, selectionGlyph } from "./glyphs.js";
|
|
18
24
|
import { safeMarkdownTheme, sentinelsFor } from "./helpers.js";
|
|
@@ -53,6 +59,8 @@ export class AskQuestionnaire extends Container {
|
|
|
53
59
|
private inputMode = false;
|
|
54
60
|
private editor?: Editor;
|
|
55
61
|
private mdTheme = safeMarkdownTheme();
|
|
62
|
+
private pager = new ModalPager();
|
|
63
|
+
private selectedOptionRows: { start: number; end: number } | undefined;
|
|
56
64
|
|
|
57
65
|
constructor(
|
|
58
66
|
params: Params,
|
|
@@ -198,6 +206,7 @@ export class AskQuestionnaire extends Container {
|
|
|
198
206
|
this.inputMode = false;
|
|
199
207
|
this.selectedOptionIndex = 0;
|
|
200
208
|
this.editor = undefined;
|
|
209
|
+
this.pager.reset();
|
|
201
210
|
this.restoreAnswerState();
|
|
202
211
|
this.refresh();
|
|
203
212
|
}
|
|
@@ -255,6 +264,10 @@ export class AskQuestionnaire extends Container {
|
|
|
255
264
|
// cancel guard, which is also bound to esc and would otherwise close the
|
|
256
265
|
// whole questionnaire instead of stepping back.
|
|
257
266
|
if (this.inputMode) {
|
|
267
|
+
if (this.pager.handleInput(data, this.keybindings)) {
|
|
268
|
+
this.refresh();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
258
271
|
if (matchesKey(data, Key.escape)) {
|
|
259
272
|
this.inputMode = false;
|
|
260
273
|
this.editor = undefined;
|
|
@@ -275,6 +288,11 @@ export class AskQuestionnaire extends Container {
|
|
|
275
288
|
return;
|
|
276
289
|
}
|
|
277
290
|
|
|
291
|
+
if (this.pager.handleInput(data, this.keybindings)) {
|
|
292
|
+
this.refresh();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
278
296
|
const isMulti = !!this.currentQ.multiSelect;
|
|
279
297
|
const total = this.totalItems;
|
|
280
298
|
|
|
@@ -285,6 +303,7 @@ export class AskQuestionnaire extends Container {
|
|
|
285
303
|
) {
|
|
286
304
|
if (total > 0) {
|
|
287
305
|
this.selectedOptionIndex = (this.selectedOptionIndex - 1 + total) % total;
|
|
306
|
+
this.pager.followSelection();
|
|
288
307
|
this.refresh();
|
|
289
308
|
}
|
|
290
309
|
return;
|
|
@@ -297,6 +316,7 @@ export class AskQuestionnaire extends Container {
|
|
|
297
316
|
) {
|
|
298
317
|
if (total > 0) {
|
|
299
318
|
this.selectedOptionIndex = (this.selectedOptionIndex + 1) % total;
|
|
319
|
+
this.pager.followSelection();
|
|
300
320
|
this.refresh();
|
|
301
321
|
}
|
|
302
322
|
return;
|
|
@@ -320,6 +340,7 @@ export class AskQuestionnaire extends Container {
|
|
|
320
340
|
chars.pop();
|
|
321
341
|
this.searchQuery = chars.join("");
|
|
322
342
|
this.selectedOptionIndex = 0;
|
|
343
|
+
this.pager.followSelection();
|
|
323
344
|
this.refresh();
|
|
324
345
|
}
|
|
325
346
|
return;
|
|
@@ -329,6 +350,7 @@ export class AskQuestionnaire extends Container {
|
|
|
329
350
|
if (this.searchQuery) {
|
|
330
351
|
this.searchQuery = "";
|
|
331
352
|
this.selectedOptionIndex = 0;
|
|
353
|
+
this.pager.followSelection();
|
|
332
354
|
this.refresh();
|
|
333
355
|
}
|
|
334
356
|
return;
|
|
@@ -376,6 +398,7 @@ export class AskQuestionnaire extends Container {
|
|
|
376
398
|
if (printable !== undefined) {
|
|
377
399
|
this.searchQuery += printable;
|
|
378
400
|
this.selectedOptionIndex = 0;
|
|
401
|
+
this.pager.followSelection();
|
|
379
402
|
this.refresh();
|
|
380
403
|
}
|
|
381
404
|
}
|
|
@@ -409,14 +432,13 @@ export class AskQuestionnaire extends Container {
|
|
|
409
432
|
|
|
410
433
|
if (total === 0) return [t.fg("warning", "No options")];
|
|
411
434
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
);
|
|
417
|
-
const end = Math.min(start + maxVisible, total);
|
|
435
|
+
// Render the complete logical list. frameModal owns the only viewport, so
|
|
436
|
+
// PageUp/PageDown can inspect every option without a nested 12-item crop.
|
|
437
|
+
const start = 0;
|
|
438
|
+
const end = total;
|
|
418
439
|
|
|
419
440
|
const lines: string[] = [];
|
|
441
|
+
this.selectedOptionRows = undefined;
|
|
420
442
|
// Hang-indent descriptions under the LABEL column, not the pointer.
|
|
421
443
|
// Prefix is `→ G ` = ptr(1)+sp(1)+glyph(1)+sp(1) = 4 cols.
|
|
422
444
|
const LABEL_COL = 4;
|
|
@@ -428,6 +450,7 @@ export class AskQuestionnaire extends Container {
|
|
|
428
450
|
const sel = i === this.selectedOptionIndex;
|
|
429
451
|
const ptr = sel ? t.fg("accent", "→") : " ";
|
|
430
452
|
|
|
453
|
+
const itemStart = lines.length;
|
|
431
454
|
if (item.kind === "option" && item.option) {
|
|
432
455
|
const optIdx = this.filteredOptions.indexOf(item.option);
|
|
433
456
|
const glyph = glyphFor(optIdx, sel);
|
|
@@ -455,12 +478,8 @@ export class AskQuestionnaire extends Container {
|
|
|
455
478
|
: t.fg("text", t.bold(SENTINEL_NEXT));
|
|
456
479
|
lines.push(truncateToWidth(`${ptr} ${t.fg("dim", "→")} ${label}`, inner, ""));
|
|
457
480
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (start > 0 || end < total) {
|
|
461
|
-
const count =
|
|
462
|
-
this.filteredOptions.length > 0 ? `${this.selectedOptionIndex + 1}/${total}` : `${total}`;
|
|
463
|
-
lines.push(t.fg("dim", truncateToWidth(` ${count}`, inner, "")));
|
|
481
|
+
if (sel)
|
|
482
|
+
this.selectedOptionRows = { start: itemStart, end: Math.max(itemStart + 1, lines.length) };
|
|
464
483
|
}
|
|
465
484
|
|
|
466
485
|
return lines;
|
|
@@ -499,7 +518,9 @@ export class AskQuestionnaire extends Container {
|
|
|
499
518
|
const leftWidth = useSplit ? Math.floor((width - 2) * 0.45) : inner;
|
|
500
519
|
const previewWidth = useSplit ? Math.max(20, width - leftWidth - 3) : 0;
|
|
501
520
|
|
|
502
|
-
const
|
|
521
|
+
const header: string[] = [];
|
|
522
|
+
const body: string[] = [];
|
|
523
|
+
let footer: string[] = [];
|
|
503
524
|
|
|
504
525
|
const row = (content: string): string => truncateToWidth(content, width, "");
|
|
505
526
|
|
|
@@ -521,25 +542,24 @@ export class AskQuestionnaire extends Container {
|
|
|
521
542
|
this.params.questions.length > 1
|
|
522
543
|
? dim(t)(` ${this.currentIndex + 1}/${this.params.questions.length}`)
|
|
523
544
|
: "";
|
|
524
|
-
|
|
545
|
+
header.push(row(`${chip}${prog}`));
|
|
525
546
|
|
|
526
547
|
// Question text
|
|
527
548
|
for (const w of wrapTextWithAnsi(this.currentQ.question, Math.max(10, inner))) {
|
|
528
|
-
|
|
549
|
+
header.push(row(t.fg("text", t.bold(w))));
|
|
529
550
|
}
|
|
530
551
|
|
|
531
552
|
// Input mode
|
|
532
553
|
if (this.inputMode) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
return this.frame(mw, lines, top);
|
|
554
|
+
header.push("");
|
|
555
|
+
header.push(row(t.fg("accent", t.bold("Type your response:"))));
|
|
556
|
+
body.push(
|
|
557
|
+
...this.ensureEditor()
|
|
558
|
+
.render(width)
|
|
559
|
+
.map((line) => truncateToWidth(line, width, "")),
|
|
560
|
+
);
|
|
561
|
+
footer = [row(dim(t)("PgUp/PgDn inspect • enter submit • esc back • ctrl+c cancel"))];
|
|
562
|
+
return this.frame(mw, header, body, footer, top);
|
|
543
563
|
}
|
|
544
564
|
|
|
545
565
|
// Search bar
|
|
@@ -547,7 +567,7 @@ export class AskQuestionnaire extends Container {
|
|
|
547
567
|
const searchVal = this.searchQuery
|
|
548
568
|
? t.fg("text", this.searchQuery)
|
|
549
569
|
: t.fg("dim", "type to filter");
|
|
550
|
-
|
|
570
|
+
header.push(row(`${t.fg("accent", "Filter:")} ${searchVal}`));
|
|
551
571
|
}
|
|
552
572
|
|
|
553
573
|
// Options (with optional split-pane preview)
|
|
@@ -560,11 +580,11 @@ export class AskQuestionnaire extends Container {
|
|
|
560
580
|
for (let i = 0; i < maxOptLines; i++) {
|
|
561
581
|
const left = truncateToWidth(optionLines[i] ?? "", leftWidth, "", true);
|
|
562
582
|
const right = truncateToWidth(previewLines[i] ?? "", previewWidth, "");
|
|
563
|
-
const
|
|
564
|
-
|
|
583
|
+
const splitRow = `${left || " ".repeat(leftWidth)}${sep}${right || " ".repeat(previewWidth)}`;
|
|
584
|
+
body.push(truncateToWidth(splitRow, width, ""));
|
|
565
585
|
}
|
|
566
586
|
} else {
|
|
567
|
-
for (const line of optionLines)
|
|
587
|
+
for (const line of optionLines) body.push(row(line));
|
|
568
588
|
}
|
|
569
589
|
|
|
570
590
|
// Footer hints
|
|
@@ -572,20 +592,39 @@ export class AskQuestionnaire extends Container {
|
|
|
572
592
|
const hintParts = isMulti
|
|
573
593
|
? [`${navHint} • space toggle • enter commit • esc clear`, "ctrl+c cancel"]
|
|
574
594
|
: [`${navHint} • type filter • enter select • esc clear`, "ctrl+c cancel"];
|
|
575
|
-
|
|
595
|
+
footer = [row(dim(t)(`${hintParts.join(" • ")} • PgUp/PgDn inspect`))];
|
|
576
596
|
|
|
577
|
-
return this.frame(mw,
|
|
597
|
+
return this.frame(mw, header, body, footer, top);
|
|
578
598
|
}
|
|
579
599
|
|
|
580
|
-
/**
|
|
581
|
-
private frame(
|
|
600
|
+
/** Frame sections with a terminal-height budget and inspectable body paging. */
|
|
601
|
+
private frame(
|
|
602
|
+
outerWidth: number,
|
|
603
|
+
header: string[],
|
|
604
|
+
body: string[],
|
|
605
|
+
footer: string[],
|
|
606
|
+
top: string | undefined,
|
|
607
|
+
): string[] {
|
|
582
608
|
const t = this.theme;
|
|
583
|
-
|
|
609
|
+
const result = frameModal({
|
|
584
610
|
width: outerWidth,
|
|
585
|
-
|
|
611
|
+
maxHeight: terminalModalHeight(this.tui.terminal.rows),
|
|
612
|
+
minHeight: MIN_MODAL_HEIGHT,
|
|
613
|
+
header,
|
|
614
|
+
body,
|
|
615
|
+
footer,
|
|
616
|
+
bodyOffset: this.pager.bodyOffset,
|
|
617
|
+
selectedBodyRange:
|
|
618
|
+
this.selectedOptionRows === undefined
|
|
619
|
+
? undefined
|
|
620
|
+
: this.pager.selectedRange(this.selectedOptionRows),
|
|
586
621
|
top,
|
|
587
622
|
color: (s) => t.fg("accent", s),
|
|
588
623
|
bg: (s) => t.bg("customMessageBg", s),
|
|
624
|
+
overflowLine: ({ page, totalPages }) =>
|
|
625
|
+
t.fg("dim", `PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
589
626
|
});
|
|
627
|
+
this.pager.sync(result);
|
|
628
|
+
return result.lines;
|
|
590
629
|
}
|
|
591
630
|
}
|