@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.js CHANGED
@@ -84,7 +84,43 @@ function ScreenFooter({ lines, textStyle }) {
84
84
  // src/screen/list-components.js
85
85
  import React, { useState, useEffect, useRef, createElement } from "react";
86
86
  import { Box as Box2, Text as Text2 } from "ink";
87
+
88
+ // src/screen/scrollbar.js
89
+ var BAR_THUMB = "#";
90
+ var BAR_TRACK = "|";
91
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
92
+ const view = Math.max(1, Math.floor(viewportRows));
93
+ const total = Math.max(0, Math.floor(totalLines));
94
+ if (total <= view) return null;
95
+ const maxScroll = total - view;
96
+ const thumbSize = Math.max(1, Math.round(view / total * view));
97
+ const travel = Math.max(0, view - thumbSize);
98
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
99
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
100
+ const glyphs = [];
101
+ for (let i = 0; i < view; i++) {
102
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
103
+ }
104
+ return glyphs;
105
+ }
106
+ var PAGE_SCROLL_KEY_BINDINGS = [
107
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
108
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
109
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
110
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
111
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
112
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
113
+ ];
114
+
115
+ // src/screen/list-components.js
87
116
  var h2 = createElement;
117
+ function clipNameForScrollbar(name, needsBar) {
118
+ if (!needsBar || name == null) return name;
119
+ const s = String(name);
120
+ if (s.length <= 1) return s;
121
+ if (/\s$/.test(s)) return s.slice(0, -1);
122
+ return s;
123
+ }
88
124
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
89
125
  const [, forceUpdate] = useState({});
90
126
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -298,17 +334,42 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
298
334
  });
299
335
  ctx.setAction("scrollUp", () => {
300
336
  const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
301
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
302
- setScrollOffset(newScrollOffset);
337
+ setScrollOffset(Math.max(0, currentScrollOffset - 1));
303
338
  forceUpdate({});
304
339
  });
305
340
  ctx.setAction("scrollDown", () => {
306
341
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
307
342
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
308
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
309
- setScrollOffset(newScrollOffset);
343
+ setScrollOffset(Math.min(currentMaxScrollOffset, currentScrollOffset + 1));
310
344
  forceUpdate({});
311
345
  });
346
+ ctx.setAction("pageUp", () => {
347
+ const { maxHeight: vh } = scrollStateRef.current;
348
+ const page = Math.max(1, vh || 1);
349
+ const newIndex = Math.max(0, selectedIndexRef.current - page);
350
+ selectedIndexRef.current = newIndex;
351
+ const list = displayItemsRef.current;
352
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
353
+ setScrollOffset(newIndex);
354
+ forceUpdate({});
355
+ });
356
+ ctx.setAction("pageDown", () => {
357
+ const { maxHeight: vh, totalItems } = scrollStateRef.current;
358
+ const page = Math.max(1, vh || 1);
359
+ const maxIndex = Math.max(0, totalItems - 1);
360
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + page);
361
+ selectedIndexRef.current = newIndex;
362
+ const list = displayItemsRef.current;
363
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
364
+ const maxScroll = Math.max(0, totalItems - page);
365
+ setScrollOffset(Math.min(maxScroll, Math.max(0, newIndex - page + 1)));
366
+ forceUpdate({});
367
+ });
368
+ const navBindings = [
369
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
370
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
371
+ ...PAGE_SCROLL_KEY_BINDINGS
372
+ ];
312
373
  if (sortable) {
313
374
  ctx.setAction("toggleSort", () => {
314
375
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
@@ -355,8 +416,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
355
416
  }
356
417
  };
357
418
  ctx.setKeyBinding([
358
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
359
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
419
+ ...navBindings,
360
420
  {
361
421
  key: "s",
362
422
  caption: sortCaption,
@@ -366,47 +426,47 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
366
426
  ]);
367
427
  ctx.update();
368
428
  } else {
369
- ctx.setKeyBinding([
370
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
371
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
372
- ]);
429
+ ctx.setKeyBinding(navBindings);
373
430
  }
374
431
  }, [sortOrder, sortable]);
375
432
  const selectedIndex = selectedIndexRef.current;
433
+ const needsBar = displayItems.length > effectiveMaxHeight;
434
+ const barGlyphs = needsBar ? scrollbarGlyphs(effectiveMaxHeight, displayItems.length, clampedScrollOffset) : null;
435
+ const appendBar = (rowContent, displayIndex) => {
436
+ if (!barGlyphs) return rowContent;
437
+ const glyph = barGlyphs[displayIndex] ?? BAR_TRACK;
438
+ return h2(
439
+ Box2,
440
+ { flexDirection: "row" },
441
+ rowContent,
442
+ h2(Text2, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
443
+ );
444
+ };
376
445
  const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
377
446
  const isFirstVisible = displayIndex === 0;
378
447
  const isLastVisible = displayIndex === visibleItems.length - 1;
379
- let arrowPrefix = "";
380
- let selectionPrefix = "";
381
- if (isFirstVisible && canScrollUp) {
382
- arrowPrefix = "\u2191 ";
383
- } else if (isLastVisible && canScrollDown) {
384
- arrowPrefix = "\u2193 ";
385
- } else {
386
- arrowPrefix = " ";
387
- }
388
- if (isSelected) {
389
- selectionPrefix = selectionMarker;
390
- } else {
391
- selectionPrefix = " ".repeat(selectionMarker.length);
448
+ let arrowPrefix = " ";
449
+ if (!needsBar) {
450
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
451
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
392
452
  }
453
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
454
+ const label = clipNameForScrollbar(item.name, needsBar);
393
455
  return h2(
394
456
  Box2,
395
457
  { flexDirection: "row" },
396
- // Arrow (clickable if functional, not highlighted)
397
- h2(Text2, {
398
- key: `arrow-${actualIndex}`,
399
- color: "white"
400
- }, arrowPrefix),
401
- // Selection marker space (always same width, not highlighted)
458
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
402
459
  h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
403
- // Item name (highlighted if selected)
404
- h2(Text2, {
405
- key: `name-${actualIndex}`,
406
- color: isSelected ? "black" : "white",
407
- backgroundColor: isSelected ? "cyan" : void 0,
408
- bold: isSelected
409
- }, item.name)
460
+ h2(
461
+ Text2,
462
+ {
463
+ key: `name-${actualIndex}`,
464
+ color: isSelected ? "black" : "white",
465
+ backgroundColor: isSelected ? "cyan" : void 0,
466
+ bold: isSelected
467
+ },
468
+ label
469
+ )
410
470
  );
411
471
  };
412
472
  const itemRenderer = renderItem || defaultRenderItem;
@@ -419,42 +479,32 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
419
479
  if (renderItem) {
420
480
  const isFirstVisible = displayIndex === 0;
421
481
  const isLastVisible = displayIndex === visibleItems.length - 1;
422
- let arrowPrefix = "";
423
- let selectionPrefix = "";
424
- if (isFirstVisible && canScrollUp) {
425
- arrowPrefix = "\u2191 ";
426
- } else if (isLastVisible && canScrollDown) {
427
- arrowPrefix = "\u2193 ";
428
- } else {
429
- arrowPrefix = " ";
430
- }
431
- if (isSelected) {
432
- selectionPrefix = selectionMarker;
433
- } else {
434
- selectionPrefix = " ".repeat(selectionMarker.length);
482
+ let arrowPrefix = " ";
483
+ if (!needsBar) {
484
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
485
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
435
486
  }
487
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
488
+ const clipped = needsBar ? { ...item, name: clipNameForScrollbar(item.name, true) } : item;
489
+ const row = h2(
490
+ Box2,
491
+ { flexDirection: "row" },
492
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
493
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
494
+ renderItem(clipped, isSelected, displayIndex)
495
+ );
436
496
  return h2(ScreenRow, {
437
497
  key: `item-${actualIndex}`,
438
- children: h2(
439
- Box2,
440
- { flexDirection: "row" },
441
- // Arrow (clickable if functional, not highlighted)
442
- h2(Text2, {
443
- key: `arrow-${actualIndex}`,
444
- color: "white"
445
- }, arrowPrefix),
446
- // Selection marker space (always same width, not highlighted)
447
- h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
448
- // Custom rendered content
449
- renderItem(item, isSelected, displayIndex)
450
- )
451
- });
452
- } else {
453
- return h2(ScreenRow, {
454
- key: `item-${actualIndex}`,
455
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
498
+ children: appendBar(row, displayIndex)
456
499
  });
457
500
  }
501
+ return h2(ScreenRow, {
502
+ key: `item-${actualIndex}`,
503
+ children: appendBar(
504
+ itemRenderer(item, isSelected, displayIndex, actualIndex),
505
+ displayIndex
506
+ )
507
+ });
458
508
  })
459
509
  );
460
510
  }
@@ -931,21 +981,6 @@ function wrapTextLines(text, cols) {
931
981
  }
932
982
  return out;
933
983
  }
934
- function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
935
- const view = Math.max(1, Math.floor(viewportRows));
936
- const total = Math.max(0, Math.floor(totalLines));
937
- if (total <= view) return null;
938
- const maxScroll = total - view;
939
- const thumbSize = Math.max(1, Math.round(view / total * view));
940
- const travel = Math.max(0, view - thumbSize);
941
- const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
942
- const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
943
- const glyphs = [];
944
- for (let i = 0; i < view; i++) {
945
- glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
946
- }
947
- return glyphs;
948
- }
949
984
  function padEndVisible(s, w) {
950
985
  const t = String(s ?? "");
951
986
  if (t.length >= w) return t.slice(0, w);
@@ -954,12 +989,7 @@ function padEndVisible(s, w) {
954
989
  var SCROLL_KEYS = [
955
990
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
956
991
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
957
- { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
958
- { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
959
- { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
960
- { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
961
- { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
962
- { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
992
+ ...PAGE_SCROLL_KEY_BINDINGS
963
993
  ];
964
994
  function ScrollableText({
965
995
  ctx,
@@ -974,20 +1004,19 @@ function ScrollableText({
974
1004
  }) {
975
1005
  const [scrollTop, setScrollTop] = useState3(0);
976
1006
  const [, bump] = useState3(0);
977
- const termCols = process.stdout.columns || 80;
978
1007
  const termRows = process.stdout.rows || 24;
979
1008
  const viewportRows = Math.max(
980
1009
  4,
981
1010
  maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
982
1011
  );
983
- const provisionalBar = showScrollbar ? 1 : 0;
984
- const wrapCols = Math.max(8, termCols - provisionalBar);
1012
+ const contentCols = Math.max(20, getScreenWidth() - 4);
1013
+ const barCols = showScrollbar ? 1 : 0;
1014
+ const textWidth = Math.max(8, contentCols - barCols);
985
1015
  const allLines = useMemo(() => {
986
1016
  if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
987
- return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
988
- }, [linesProp, text, wrap, wrapCols]);
1017
+ return wrap ? wrapTextLines(text, textWidth) : String(text ?? "").split("\n");
1018
+ }, [linesProp, text, wrap, textWidth]);
989
1019
  const needsBar = showScrollbar && allLines.length > viewportRows;
990
- const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
991
1020
  const maxScroll = Math.max(0, allLines.length - viewportRows);
992
1021
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
993
1022
  const visible = allLines.slice(clamped, clamped + viewportRows);
@@ -1027,22 +1056,24 @@ function ScrollableText({
1027
1056
  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" : "");
1028
1057
  const rowNodes = visible.map((line, i) => {
1029
1058
  const body = padEndVisible(line, textWidth);
1030
- const glyph = bar ? bar[i] ?? "\u2502" : "";
1059
+ const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1060
+ const isThumb = glyph === BAR_THUMB;
1031
1061
  return h5(
1032
- Box4,
1033
- { key: `L${clamped + i}`, flexDirection: "row" },
1034
- h5(Text5, {}, body),
1035
- glyph ? h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1062
+ Text5,
1063
+ { key: `L${clamped + i}` },
1064
+ body,
1065
+ glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1036
1066
  );
1037
1067
  });
1038
1068
  if (bar && visible.length < viewportRows) {
1039
1069
  for (let i = visible.length; i < viewportRows; i++) {
1070
+ const glyph = bar[i] ?? BAR_TRACK;
1040
1071
  rowNodes.push(
1041
1072
  h5(
1042
- Box4,
1043
- { key: `pad${i}`, flexDirection: "row" },
1044
- h5(Text5, {}, padEndVisible("", textWidth)),
1045
- h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1073
+ Text5,
1074
+ { key: `pad${i}` },
1075
+ padEndVisible("", textWidth),
1076
+ h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1046
1077
  )
1047
1078
  );
1048
1079
  }
@@ -1191,6 +1222,8 @@ if (typeof window === "undefined") {
1191
1222
  });
1192
1223
  }
1193
1224
  export {
1225
+ BAR_THUMB,
1226
+ BAR_TRACK,
1194
1227
  Box5 as Box,
1195
1228
  Divider,
1196
1229
  FooterPresets,
@@ -1200,6 +1233,7 @@ export {
1200
1233
  ListItem,
1201
1234
  MultiColumnListComponent,
1202
1235
  MultiColumnListWithPreviewComponent,
1236
+ PAGE_SCROLL_KEY_BINDINGS,
1203
1237
  React2 as React,
1204
1238
  ScreenBody,
1205
1239
  ScreenContainer,