@xynogen/pix-ask 0.2.11 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-ask",
3
- "version": "0.2.11",
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",
@@ -12,7 +12,13 @@ import {
12
12
  truncateToWidth,
13
13
  wrapTextWithAnsi,
14
14
  } from "@earendil-works/pi-tui";
15
- import { frameLines, modalWidth } from "@xynogen/pix-pretty/modal-frame";
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
- const maxVisible = Math.min(total, 12);
413
- const start = Math.max(
414
- 0,
415
- Math.min(this.selectedOptionIndex - Math.floor(maxVisible / 2), total - maxVisible),
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 lines: string[] = [];
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
- lines.push(row(`${chip}${prog}`));
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
- lines.push(row(t.fg("text", t.bold(w))));
549
+ header.push(row(t.fg("text", t.bold(w))));
529
550
  }
530
551
 
531
552
  // Input mode
532
553
  if (this.inputMode) {
533
- lines.push("");
534
- lines.push(row(t.fg("accent", t.bold("Type your response:"))));
535
- lines.push("");
536
- const editorLines = this.ensureEditor().render(width);
537
- for (const el of editorLines) {
538
- lines.push(truncateToWidth(el, width, ""));
539
- }
540
- lines.push("");
541
- lines.push(row(dim(t)("enter submit esc back • ctrl+c cancel")));
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
- lines.push(row(`${t.fg("accent", "Filter:")} ${searchVal}`));
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 body = `${left || " ".repeat(leftWidth)}${sep}${right || " ".repeat(previewWidth)}`;
564
- lines.push(truncateToWidth(body, width, ""));
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) lines.push(row(line));
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
- lines.push(row(dim(t)(hintParts.join(" • "))));
595
+ footer = [row(dim(t)(`${hintParts.join(" • ")} • PgUp/PgDn inspect`))];
576
596
 
577
- return this.frame(mw, lines, top);
597
+ return this.frame(mw, header, body, footer, top);
578
598
  }
579
599
 
580
- /** Wrap body lines in the rounded modal border at the given outer width. */
581
- private frame(outerWidth: number, lines: string[], top: string | undefined): string[] {
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
- return frameLines({
609
+ const result = frameModal({
584
610
  width: outerWidth,
585
- lines,
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
  }