@nmakarov/cli-toolkit 0.68.0 → 0.70.0

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/dist/screen.cjs CHANGED
@@ -29,6 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/screen/index.js
30
30
  var screen_exports = {};
31
31
  __export(screen_exports, {
32
+ BAR_THUMB: () => BAR_THUMB,
33
+ BAR_TRACK: () => BAR_TRACK,
32
34
  Box: () => import_ink6.Box,
33
35
  Divider: () => Divider,
34
36
  FooterPresets: () => FooterPresets,
@@ -38,6 +40,7 @@ __export(screen_exports, {
38
40
  ListItem: () => ListItem,
39
41
  MultiColumnListComponent: () => MultiColumnListComponent,
40
42
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
43
+ PAGE_SCROLL_KEY_BINDINGS: () => PAGE_SCROLL_KEY_BINDINGS,
41
44
  React: () => import_react6.default,
42
45
  ScreenBody: () => ScreenBody,
43
46
  ScreenContainer: () => ScreenContainer,
@@ -160,7 +163,43 @@ function ScreenFooter({ lines, textStyle }) {
160
163
  // src/screen/list-components.js
161
164
  var import_react2 = null; // Will be set by load();
162
165
  var import_ink2 = null; // Will be set by load();
166
+
167
+ // src/screen/scrollbar.js
168
+ var BAR_THUMB = "#";
169
+ var BAR_TRACK = "|";
170
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
171
+ const view = Math.max(1, Math.floor(viewportRows));
172
+ const total = Math.max(0, Math.floor(totalLines));
173
+ if (total <= view) return null;
174
+ const maxScroll = total - view;
175
+ const thumbSize = Math.max(1, Math.round(view / total * view));
176
+ const travel = Math.max(0, view - thumbSize);
177
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
178
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
179
+ const glyphs = [];
180
+ for (let i = 0; i < view; i++) {
181
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
182
+ }
183
+ return glyphs;
184
+ }
185
+ var PAGE_SCROLL_KEY_BINDINGS = [
186
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
187
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
188
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
189
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
190
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
191
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
192
+ ];
193
+
194
+ // src/screen/list-components.js
163
195
  // Removed: intermediate assignment - code will access import variable directly
196
+ function clipNameForScrollbar(name, needsBar) {
197
+ if (!needsBar || name == null) return name;
198
+ const s = String(name);
199
+ if (s.length <= 1) return s;
200
+ if (/\s$/.test(s)) return s.slice(0, -1);
201
+ return s;
202
+ }
164
203
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
165
204
  const [, forceUpdate] = (0, import_react2.useState)({});
166
205
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -374,17 +413,42 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
374
413
  });
375
414
  ctx.setAction("scrollUp", () => {
376
415
  const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
377
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
378
- setScrollOffset(newScrollOffset);
416
+ setScrollOffset(Math.max(0, currentScrollOffset - 1));
379
417
  forceUpdate({});
380
418
  });
381
419
  ctx.setAction("scrollDown", () => {
382
420
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
383
421
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
384
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
385
- setScrollOffset(newScrollOffset);
422
+ setScrollOffset(Math.min(currentMaxScrollOffset, currentScrollOffset + 1));
386
423
  forceUpdate({});
387
424
  });
425
+ ctx.setAction("pageUp", () => {
426
+ const { maxHeight: vh } = scrollStateRef.current;
427
+ const page = Math.max(1, vh || 1);
428
+ const newIndex = Math.max(0, selectedIndexRef.current - page);
429
+ selectedIndexRef.current = newIndex;
430
+ const list = displayItemsRef.current;
431
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
432
+ setScrollOffset(newIndex);
433
+ forceUpdate({});
434
+ });
435
+ ctx.setAction("pageDown", () => {
436
+ const { maxHeight: vh, totalItems } = scrollStateRef.current;
437
+ const page = Math.max(1, vh || 1);
438
+ const maxIndex = Math.max(0, totalItems - 1);
439
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + page);
440
+ selectedIndexRef.current = newIndex;
441
+ const list = displayItemsRef.current;
442
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
443
+ const maxScroll = Math.max(0, totalItems - page);
444
+ setScrollOffset(Math.min(maxScroll, Math.max(0, newIndex - page + 1)));
445
+ forceUpdate({});
446
+ });
447
+ const navBindings = [
448
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
449
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
450
+ ...PAGE_SCROLL_KEY_BINDINGS
451
+ ];
388
452
  if (sortable) {
389
453
  ctx.setAction("toggleSort", () => {
390
454
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
@@ -431,8 +495,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
431
495
  }
432
496
  };
433
497
  ctx.setKeyBinding([
434
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
435
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
498
+ ...navBindings,
436
499
  {
437
500
  key: "s",
438
501
  caption: sortCaption,
@@ -442,47 +505,47 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
442
505
  ]);
443
506
  ctx.update();
444
507
  } else {
445
- ctx.setKeyBinding([
446
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
447
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
448
- ]);
508
+ ctx.setKeyBinding(navBindings);
449
509
  }
450
510
  }, [sortOrder, sortable]);
451
511
  const selectedIndex = selectedIndexRef.current;
512
+ const needsBar = displayItems.length > effectiveMaxHeight;
513
+ const barGlyphs = needsBar ? scrollbarGlyphs(effectiveMaxHeight, displayItems.length, clampedScrollOffset) : null;
514
+ const appendBar = (rowContent, displayIndex) => {
515
+ if (!barGlyphs) return rowContent;
516
+ const glyph = barGlyphs[displayIndex] ?? BAR_TRACK;
517
+ return h2(
518
+ import_ink2.Box,
519
+ { flexDirection: "row" },
520
+ rowContent,
521
+ h2(import_ink2.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
522
+ );
523
+ };
452
524
  const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
453
525
  const isFirstVisible = displayIndex === 0;
454
526
  const isLastVisible = displayIndex === visibleItems.length - 1;
455
- let arrowPrefix = "";
456
- let selectionPrefix = "";
457
- if (isFirstVisible && canScrollUp) {
458
- arrowPrefix = "\u2191 ";
459
- } else if (isLastVisible && canScrollDown) {
460
- arrowPrefix = "\u2193 ";
461
- } else {
462
- arrowPrefix = " ";
463
- }
464
- if (isSelected) {
465
- selectionPrefix = selectionMarker;
466
- } else {
467
- selectionPrefix = " ".repeat(selectionMarker.length);
527
+ let arrowPrefix = " ";
528
+ if (!needsBar) {
529
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
530
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
468
531
  }
532
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
533
+ const label = clipNameForScrollbar(item.name, needsBar);
469
534
  return h2(
470
535
  import_ink2.Box,
471
536
  { flexDirection: "row" },
472
- // Arrow (clickable if functional, not highlighted)
473
- h2(import_ink2.Text, {
474
- key: `arrow-${actualIndex}`,
475
- color: "white"
476
- }, arrowPrefix),
477
- // Selection marker space (always same width, not highlighted)
537
+ h2(import_ink2.Text, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
478
538
  h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
479
- // Item name (highlighted if selected)
480
- h2(import_ink2.Text, {
481
- key: `name-${actualIndex}`,
482
- color: isSelected ? "black" : "white",
483
- backgroundColor: isSelected ? "cyan" : void 0,
484
- bold: isSelected
485
- }, item.name)
539
+ h2(
540
+ import_ink2.Text,
541
+ {
542
+ key: `name-${actualIndex}`,
543
+ color: isSelected ? "black" : "white",
544
+ backgroundColor: isSelected ? "cyan" : void 0,
545
+ bold: isSelected
546
+ },
547
+ label
548
+ )
486
549
  );
487
550
  };
488
551
  const itemRenderer = renderItem || defaultRenderItem;
@@ -495,42 +558,32 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
495
558
  if (renderItem) {
496
559
  const isFirstVisible = displayIndex === 0;
497
560
  const isLastVisible = displayIndex === visibleItems.length - 1;
498
- let arrowPrefix = "";
499
- let selectionPrefix = "";
500
- if (isFirstVisible && canScrollUp) {
501
- arrowPrefix = "\u2191 ";
502
- } else if (isLastVisible && canScrollDown) {
503
- arrowPrefix = "\u2193 ";
504
- } else {
505
- arrowPrefix = " ";
506
- }
507
- if (isSelected) {
508
- selectionPrefix = selectionMarker;
509
- } else {
510
- selectionPrefix = " ".repeat(selectionMarker.length);
561
+ let arrowPrefix = " ";
562
+ if (!needsBar) {
563
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
564
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
511
565
  }
566
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
567
+ const clipped = needsBar ? { ...item, name: clipNameForScrollbar(item.name, true) } : item;
568
+ const row = h2(
569
+ import_ink2.Box,
570
+ { flexDirection: "row" },
571
+ h2(import_ink2.Text, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
572
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
573
+ renderItem(clipped, isSelected, displayIndex)
574
+ );
512
575
  return h2(ScreenRow, {
513
576
  key: `item-${actualIndex}`,
514
- children: h2(
515
- import_ink2.Box,
516
- { flexDirection: "row" },
517
- // Arrow (clickable if functional, not highlighted)
518
- h2(import_ink2.Text, {
519
- key: `arrow-${actualIndex}`,
520
- color: "white"
521
- }, arrowPrefix),
522
- // Selection marker space (always same width, not highlighted)
523
- h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
524
- // Custom rendered content
525
- renderItem(item, isSelected, displayIndex)
526
- )
527
- });
528
- } else {
529
- return h2(ScreenRow, {
530
- key: `item-${actualIndex}`,
531
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
577
+ children: appendBar(row, displayIndex)
532
578
  });
533
579
  }
580
+ return h2(ScreenRow, {
581
+ key: `item-${actualIndex}`,
582
+ children: appendBar(
583
+ itemRenderer(item, isSelected, displayIndex, actualIndex),
584
+ displayIndex
585
+ )
586
+ });
534
587
  })
535
588
  );
536
589
  }
@@ -1007,21 +1060,6 @@ function wrapTextLines(text, cols) {
1007
1060
  }
1008
1061
  return out;
1009
1062
  }
1010
- function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
1011
- const view = Math.max(1, Math.floor(viewportRows));
1012
- const total = Math.max(0, Math.floor(totalLines));
1013
- if (total <= view) return null;
1014
- const maxScroll = total - view;
1015
- const thumbSize = Math.max(1, Math.round(view / total * view));
1016
- const travel = Math.max(0, view - thumbSize);
1017
- const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
1018
- const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
1019
- const glyphs = [];
1020
- for (let i = 0; i < view; i++) {
1021
- glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
1022
- }
1023
- return glyphs;
1024
- }
1025
1063
  function padEndVisible(s, w) {
1026
1064
  const t = String(s ?? "");
1027
1065
  if (t.length >= w) return t.slice(0, w);
@@ -1030,12 +1068,7 @@ function padEndVisible(s, w) {
1030
1068
  var SCROLL_KEYS = [
1031
1069
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1032
1070
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1033
- { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1034
- { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1035
- { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1036
- { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1037
- { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1038
- { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1071
+ ...PAGE_SCROLL_KEY_BINDINGS
1039
1072
  ];
1040
1073
  function ScrollableText({
1041
1074
  ctx,
@@ -1050,20 +1083,19 @@ function ScrollableText({
1050
1083
  }) {
1051
1084
  const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
1052
1085
  const [, bump] = (0, import_react5.useState)(0);
1053
- const termCols = process.stdout.columns || 80;
1054
1086
  const termRows = process.stdout.rows || 24;
1055
1087
  const viewportRows = Math.max(
1056
1088
  4,
1057
1089
  maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
1058
1090
  );
1059
- const provisionalBar = showScrollbar ? 1 : 0;
1060
- const wrapCols = Math.max(8, termCols - provisionalBar);
1091
+ const contentCols = Math.max(20, getScreenWidth() - 4);
1092
+ const barCols = showScrollbar ? 1 : 0;
1093
+ const textWidth = Math.max(8, contentCols - barCols);
1061
1094
  const allLines = (0, import_react5.useMemo)(() => {
1062
1095
  if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
1063
- return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
1064
- }, [linesProp, text, wrap, wrapCols]);
1096
+ return wrap ? wrapTextLines(text, textWidth) : String(text ?? "").split("\n");
1097
+ }, [linesProp, text, wrap, textWidth]);
1065
1098
  const needsBar = showScrollbar && allLines.length > viewportRows;
1066
- const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
1067
1099
  const maxScroll = Math.max(0, allLines.length - viewportRows);
1068
1100
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1069
1101
  const visible = allLines.slice(clamped, clamped + viewportRows);
@@ -1103,22 +1135,24 @@ function ScrollableText({
1103
1135
  const status = allLines.length === 0 ? "empty" : `lines ${clamped + 1}-${Math.min(clamped + visible.length, allLines.length)} of ${allLines.length}` + (needsBar ? " \xB7 \u2325\u2191/\u2193 or PgUp/Dn page" : "");
1104
1136
  const rowNodes = visible.map((line, i) => {
1105
1137
  const body = padEndVisible(line, textWidth);
1106
- const glyph = bar ? bar[i] ?? "\u2502" : "";
1138
+ const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1139
+ const isThumb = glyph === BAR_THUMB;
1107
1140
  return h5(
1108
- import_ink5.Box,
1109
- { key: `L${clamped + i}`, flexDirection: "row" },
1110
- h5(import_ink5.Text, {}, body),
1111
- glyph ? h5(import_ink5.Text, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1141
+ import_ink5.Text,
1142
+ { key: `L${clamped + i}` },
1143
+ body,
1144
+ glyph ? h5(import_ink5.Text, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1112
1145
  );
1113
1146
  });
1114
1147
  if (bar && visible.length < viewportRows) {
1115
1148
  for (let i = visible.length; i < viewportRows; i++) {
1149
+ const glyph = bar[i] ?? BAR_TRACK;
1116
1150
  rowNodes.push(
1117
1151
  h5(
1118
- import_ink5.Box,
1119
- { key: `pad${i}`, flexDirection: "row" },
1120
- h5(import_ink5.Text, {}, padEndVisible("", textWidth)),
1121
- h5(import_ink5.Text, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1152
+ import_ink5.Text,
1153
+ { key: `pad${i}` },
1154
+ padEndVisible("", textWidth),
1155
+ h5(import_ink5.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1122
1156
  )
1123
1157
  );
1124
1158
  }
@@ -1280,6 +1314,8 @@ if (typeof window === "undefined") {
1280
1314
  }
1281
1315
  // Annotate the CommonJS export names for ESM import in node:
1282
1316
  0 && (module.exports = {
1317
+ BAR_THUMB,
1318
+ BAR_TRACK,
1283
1319
  Box,
1284
1320
  Divider,
1285
1321
  FooterPresets,
@@ -1289,6 +1325,7 @@ if (typeof window === "undefined") {
1289
1325
  ListItem,
1290
1326
  MultiColumnListComponent,
1291
1327
  MultiColumnListWithPreviewComponent,
1328
+ PAGE_SCROLL_KEY_BINDINGS,
1292
1329
  React,
1293
1330
  ScreenBody,
1294
1331
  ScreenContainer,