@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/init.js CHANGED
@@ -93,9 +93,48 @@ var init_components = __esm({
93
93
  }
94
94
  });
95
95
 
96
+ // src/screen/scrollbar.js
97
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
98
+ const view = Math.max(1, Math.floor(viewportRows));
99
+ const total = Math.max(0, Math.floor(totalLines));
100
+ if (total <= view) return null;
101
+ const maxScroll = total - view;
102
+ const thumbSize = Math.max(1, Math.round(view / total * view));
103
+ const travel = Math.max(0, view - thumbSize);
104
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
105
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
106
+ const glyphs = [];
107
+ for (let i = 0; i < view; i++) {
108
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
109
+ }
110
+ return glyphs;
111
+ }
112
+ var BAR_THUMB, BAR_TRACK, PAGE_SCROLL_KEY_BINDINGS;
113
+ var init_scrollbar = __esm({
114
+ "src/screen/scrollbar.js"() {
115
+ BAR_THUMB = "#";
116
+ BAR_TRACK = "|";
117
+ PAGE_SCROLL_KEY_BINDINGS = [
118
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
119
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
120
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
121
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
122
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
123
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
124
+ ];
125
+ }
126
+ });
127
+
96
128
  // src/screen/list-components.js
97
129
  import React, { useState, useEffect, useRef, createElement } from "react";
98
130
  import { Box as Box2, Text as Text2 } from "ink";
131
+ function clipNameForScrollbar(name, needsBar) {
132
+ if (!needsBar || name == null) return name;
133
+ const s = String(name);
134
+ if (s.length <= 1) return s;
135
+ if (/\s$/.test(s)) return s.slice(0, -1);
136
+ return s;
137
+ }
99
138
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
100
139
  const [, forceUpdate] = useState({});
101
140
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -309,17 +348,42 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
309
348
  });
310
349
  ctx.setAction("scrollUp", () => {
311
350
  const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
312
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
313
- setScrollOffset(newScrollOffset);
351
+ setScrollOffset(Math.max(0, currentScrollOffset - 1));
314
352
  forceUpdate({});
315
353
  });
316
354
  ctx.setAction("scrollDown", () => {
317
355
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
318
356
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
319
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
320
- setScrollOffset(newScrollOffset);
357
+ setScrollOffset(Math.min(currentMaxScrollOffset, currentScrollOffset + 1));
321
358
  forceUpdate({});
322
359
  });
360
+ ctx.setAction("pageUp", () => {
361
+ const { maxHeight: vh } = scrollStateRef.current;
362
+ const page = Math.max(1, vh || 1);
363
+ const newIndex = Math.max(0, selectedIndexRef.current - page);
364
+ selectedIndexRef.current = newIndex;
365
+ const list = displayItemsRef.current;
366
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
367
+ setScrollOffset(newIndex);
368
+ forceUpdate({});
369
+ });
370
+ ctx.setAction("pageDown", () => {
371
+ const { maxHeight: vh, totalItems } = scrollStateRef.current;
372
+ const page = Math.max(1, vh || 1);
373
+ const maxIndex = Math.max(0, totalItems - 1);
374
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + page);
375
+ selectedIndexRef.current = newIndex;
376
+ const list = displayItemsRef.current;
377
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
378
+ const maxScroll = Math.max(0, totalItems - page);
379
+ setScrollOffset(Math.min(maxScroll, Math.max(0, newIndex - page + 1)));
380
+ forceUpdate({});
381
+ });
382
+ const navBindings = [
383
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
384
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
385
+ ...PAGE_SCROLL_KEY_BINDINGS
386
+ ];
323
387
  if (sortable) {
324
388
  ctx.setAction("toggleSort", () => {
325
389
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
@@ -366,8 +430,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
366
430
  }
367
431
  };
368
432
  ctx.setKeyBinding([
369
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
370
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
433
+ ...navBindings,
371
434
  {
372
435
  key: "s",
373
436
  caption: sortCaption,
@@ -377,47 +440,47 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
377
440
  ]);
378
441
  ctx.update();
379
442
  } else {
380
- ctx.setKeyBinding([
381
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
382
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
383
- ]);
443
+ ctx.setKeyBinding(navBindings);
384
444
  }
385
445
  }, [sortOrder, sortable]);
386
446
  const selectedIndex = selectedIndexRef.current;
447
+ const needsBar = displayItems.length > effectiveMaxHeight;
448
+ const barGlyphs = needsBar ? scrollbarGlyphs(effectiveMaxHeight, displayItems.length, clampedScrollOffset) : null;
449
+ const appendBar = (rowContent, displayIndex) => {
450
+ if (!barGlyphs) return rowContent;
451
+ const glyph = barGlyphs[displayIndex] ?? BAR_TRACK;
452
+ return h2(
453
+ Box2,
454
+ { flexDirection: "row" },
455
+ rowContent,
456
+ h2(Text2, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
457
+ );
458
+ };
387
459
  const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
388
460
  const isFirstVisible = displayIndex === 0;
389
461
  const isLastVisible = displayIndex === visibleItems.length - 1;
390
- let arrowPrefix = "";
391
- let selectionPrefix = "";
392
- if (isFirstVisible && canScrollUp) {
393
- arrowPrefix = "\u2191 ";
394
- } else if (isLastVisible && canScrollDown) {
395
- arrowPrefix = "\u2193 ";
396
- } else {
397
- arrowPrefix = " ";
398
- }
399
- if (isSelected) {
400
- selectionPrefix = selectionMarker;
401
- } else {
402
- selectionPrefix = " ".repeat(selectionMarker.length);
462
+ let arrowPrefix = " ";
463
+ if (!needsBar) {
464
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
465
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
403
466
  }
467
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
468
+ const label = clipNameForScrollbar(item.name, needsBar);
404
469
  return h2(
405
470
  Box2,
406
471
  { flexDirection: "row" },
407
- // Arrow (clickable if functional, not highlighted)
408
- h2(Text2, {
409
- key: `arrow-${actualIndex}`,
410
- color: "white"
411
- }, arrowPrefix),
412
- // Selection marker space (always same width, not highlighted)
472
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
413
473
  h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
414
- // Item name (highlighted if selected)
415
- h2(Text2, {
416
- key: `name-${actualIndex}`,
417
- color: isSelected ? "black" : "white",
418
- backgroundColor: isSelected ? "cyan" : void 0,
419
- bold: isSelected
420
- }, item.name)
474
+ h2(
475
+ Text2,
476
+ {
477
+ key: `name-${actualIndex}`,
478
+ color: isSelected ? "black" : "white",
479
+ backgroundColor: isSelected ? "cyan" : void 0,
480
+ bold: isSelected
481
+ },
482
+ label
483
+ )
421
484
  );
422
485
  };
423
486
  const itemRenderer = renderItem || defaultRenderItem;
@@ -430,42 +493,32 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
430
493
  if (renderItem) {
431
494
  const isFirstVisible = displayIndex === 0;
432
495
  const isLastVisible = displayIndex === visibleItems.length - 1;
433
- let arrowPrefix = "";
434
- let selectionPrefix = "";
435
- if (isFirstVisible && canScrollUp) {
436
- arrowPrefix = "\u2191 ";
437
- } else if (isLastVisible && canScrollDown) {
438
- arrowPrefix = "\u2193 ";
439
- } else {
440
- arrowPrefix = " ";
441
- }
442
- if (isSelected) {
443
- selectionPrefix = selectionMarker;
444
- } else {
445
- selectionPrefix = " ".repeat(selectionMarker.length);
496
+ let arrowPrefix = " ";
497
+ if (!needsBar) {
498
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
499
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
446
500
  }
501
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
502
+ const clipped = needsBar ? { ...item, name: clipNameForScrollbar(item.name, true) } : item;
503
+ const row = h2(
504
+ Box2,
505
+ { flexDirection: "row" },
506
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
507
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
508
+ renderItem(clipped, isSelected, displayIndex)
509
+ );
447
510
  return h2(ScreenRow, {
448
511
  key: `item-${actualIndex}`,
449
- children: h2(
450
- Box2,
451
- { flexDirection: "row" },
452
- // Arrow (clickable if functional, not highlighted)
453
- h2(Text2, {
454
- key: `arrow-${actualIndex}`,
455
- color: "white"
456
- }, arrowPrefix),
457
- // Selection marker space (always same width, not highlighted)
458
- h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
459
- // Custom rendered content
460
- renderItem(item, isSelected, displayIndex)
461
- )
462
- });
463
- } else {
464
- return h2(ScreenRow, {
465
- key: `item-${actualIndex}`,
466
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
512
+ children: appendBar(row, displayIndex)
467
513
  });
468
514
  }
515
+ return h2(ScreenRow, {
516
+ key: `item-${actualIndex}`,
517
+ children: appendBar(
518
+ itemRenderer(item, isSelected, displayIndex, actualIndex),
519
+ displayIndex
520
+ )
521
+ });
469
522
  })
470
523
  );
471
524
  }
@@ -473,6 +526,7 @@ var h2;
473
526
  var init_list_components = __esm({
474
527
  "src/screen/list-components.js"() {
475
528
  init_components();
529
+ init_scrollbar();
476
530
  h2 = createElement;
477
531
  }
478
532
  });
@@ -967,21 +1021,6 @@ function wrapTextLines(text, cols) {
967
1021
  }
968
1022
  return out;
969
1023
  }
970
- function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
971
- const view = Math.max(1, Math.floor(viewportRows));
972
- const total = Math.max(0, Math.floor(totalLines));
973
- if (total <= view) return null;
974
- const maxScroll = total - view;
975
- const thumbSize = Math.max(1, Math.round(view / total * view));
976
- const travel = Math.max(0, view - thumbSize);
977
- const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
978
- const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
979
- const glyphs = [];
980
- for (let i = 0; i < view; i++) {
981
- glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
982
- }
983
- return glyphs;
984
- }
985
1024
  function padEndVisible(s, w) {
986
1025
  const t = String(s ?? "");
987
1026
  if (t.length >= w) return t.slice(0, w);
@@ -1000,20 +1039,19 @@ function ScrollableText({
1000
1039
  }) {
1001
1040
  const [scrollTop, setScrollTop] = useState3(0);
1002
1041
  const [, bump] = useState3(0);
1003
- const termCols = process.stdout.columns || 80;
1004
1042
  const termRows = process.stdout.rows || 24;
1005
1043
  const viewportRows = Math.max(
1006
1044
  4,
1007
1045
  maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
1008
1046
  );
1009
- const provisionalBar = showScrollbar ? 1 : 0;
1010
- const wrapCols = Math.max(8, termCols - provisionalBar);
1047
+ const contentCols = Math.max(20, getScreenWidth() - 4);
1048
+ const barCols = showScrollbar ? 1 : 0;
1049
+ const textWidth = Math.max(8, contentCols - barCols);
1011
1050
  const allLines = useMemo(() => {
1012
1051
  if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
1013
- return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
1014
- }, [linesProp, text, wrap, wrapCols]);
1052
+ return wrap ? wrapTextLines(text, textWidth) : String(text ?? "").split("\n");
1053
+ }, [linesProp, text, wrap, textWidth]);
1015
1054
  const needsBar = showScrollbar && allLines.length > viewportRows;
1016
- const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
1017
1055
  const maxScroll = Math.max(0, allLines.length - viewportRows);
1018
1056
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1019
1057
  const visible = allLines.slice(clamped, clamped + viewportRows);
@@ -1053,22 +1091,24 @@ function ScrollableText({
1053
1091
  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" : "");
1054
1092
  const rowNodes = visible.map((line, i) => {
1055
1093
  const body = padEndVisible(line, textWidth);
1056
- const glyph = bar ? bar[i] ?? "\u2502" : "";
1094
+ const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1095
+ const isThumb = glyph === BAR_THUMB;
1057
1096
  return h5(
1058
- Box4,
1059
- { key: `L${clamped + i}`, flexDirection: "row" },
1060
- h5(Text5, {}, body),
1061
- glyph ? h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1097
+ Text5,
1098
+ { key: `L${clamped + i}` },
1099
+ body,
1100
+ glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1062
1101
  );
1063
1102
  });
1064
1103
  if (bar && visible.length < viewportRows) {
1065
1104
  for (let i = visible.length; i < viewportRows; i++) {
1105
+ const glyph = bar[i] ?? BAR_TRACK;
1066
1106
  rowNodes.push(
1067
1107
  h5(
1068
- Box4,
1069
- { key: `pad${i}`, flexDirection: "row" },
1070
- h5(Text5, {}, padEndVisible("", textWidth)),
1071
- h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1108
+ Text5,
1109
+ { key: `pad${i}` },
1110
+ padEndVisible("", textWidth),
1111
+ h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1072
1112
  )
1073
1113
  );
1074
1114
  }
@@ -1084,16 +1124,14 @@ function ScrollableText({
1084
1124
  var h5, SCROLL_KEYS;
1085
1125
  var init_scrollable_text = __esm({
1086
1126
  "src/screen/scrollable-text.js"() {
1127
+ init_components();
1128
+ init_scrollbar();
1129
+ init_scrollbar();
1087
1130
  h5 = createElement2;
1088
1131
  SCROLL_KEYS = [
1089
1132
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1090
1133
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1091
- { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1092
- { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1093
- { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1094
- { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1095
- { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1096
- { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1134
+ ...PAGE_SCROLL_KEY_BINDINGS
1097
1135
  ];
1098
1136
  }
1099
1137
  });
@@ -1229,6 +1267,8 @@ var init_footer_builder = __esm({
1229
1267
  // src/screen/index.js
1230
1268
  var screen_exports = {};
1231
1269
  __export(screen_exports, {
1270
+ BAR_THUMB: () => BAR_THUMB,
1271
+ BAR_TRACK: () => BAR_TRACK,
1232
1272
  Box: () => Box5,
1233
1273
  Divider: () => Divider,
1234
1274
  FooterPresets: () => FooterPresets,
@@ -1238,6 +1278,7 @@ __export(screen_exports, {
1238
1278
  ListItem: () => ListItem,
1239
1279
  MultiColumnListComponent: () => MultiColumnListComponent,
1240
1280
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1281
+ PAGE_SCROLL_KEY_BINDINGS: () => PAGE_SCROLL_KEY_BINDINGS,
1241
1282
  React: () => React2,
1242
1283
  ScreenBody: () => ScreenBody,
1243
1284
  ScreenContainer: () => ScreenContainer,
@@ -1293,6 +1334,7 @@ var init_screen = __esm({
1293
1334
  init_components();
1294
1335
  init_ui_elements();
1295
1336
  init_scrollable_text();
1337
+ init_scrollbar();
1296
1338
  init_key_bindings();
1297
1339
  init_utils();
1298
1340
  init_footer_builder();