@nmakarov/cli-toolkit 0.80.0 → 0.82.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
@@ -963,7 +963,66 @@ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit })
963
963
  // src/screen/scrollable-text.js
964
964
  import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
965
965
  import { Box as Box4, Text as Text5 } from "ink";
966
- var h5 = createElement2;
966
+
967
+ // src/screen/follow-scroll.js
968
+ function clampScroll(scrollTop, maxScroll) {
969
+ const max = Math.max(0, Number(maxScroll) || 0);
970
+ const top = Number(scrollTop) || 0;
971
+ return Math.min(Math.max(0, top), max);
972
+ }
973
+ function isScrolledToBottom(scrollTop, maxScroll) {
974
+ return clampScroll(scrollTop, maxScroll) >= Math.max(0, Number(maxScroll) || 0);
975
+ }
976
+ function nextScrollAfterUserMove(scrollTop, maxScroll, delta) {
977
+ const next = clampScroll((Number(scrollTop) || 0) + (Number(delta) || 0), maxScroll);
978
+ return { scrollTop: next, following: isScrolledToBottom(next, maxScroll) };
979
+ }
980
+ function nextScrollAfterContentChange({ following, scrollTop, maxScroll }) {
981
+ const max = Math.max(0, Number(maxScroll) || 0);
982
+ if (following) return { scrollTop: max, following: true };
983
+ const next = clampScroll(scrollTop, max);
984
+ return { scrollTop: next, following: isScrolledToBottom(next, max) };
985
+ }
986
+
987
+ // src/screen/visible-text.js
988
+ var ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
989
+ function stripAnsi(text) {
990
+ return String(text ?? "").replace(ANSI_RE, "");
991
+ }
992
+ function visibleWidth(text) {
993
+ return [...stripAnsi(text)].length;
994
+ }
995
+ function sliceVisible(text, cols) {
996
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
997
+ const s = String(text ?? "");
998
+ if (w === 0) return "";
999
+ let vis = 0;
1000
+ let out = "";
1001
+ let i = 0;
1002
+ while (i < s.length && vis < w) {
1003
+ ANSI_RE.lastIndex = i;
1004
+ const m = ANSI_RE.exec(s);
1005
+ if (m && m.index === i) {
1006
+ out += m[0];
1007
+ i += m[0].length;
1008
+ continue;
1009
+ }
1010
+ const cp = s.codePointAt(i);
1011
+ const ch = String.fromCodePoint(cp);
1012
+ out += ch;
1013
+ i += ch.length;
1014
+ vis += 1;
1015
+ }
1016
+ return out;
1017
+ }
1018
+ function padEndVisible(text, cols) {
1019
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1020
+ const s = String(text ?? "");
1021
+ const vis = visibleWidth(s);
1022
+ if (vis === w) return s;
1023
+ if (vis > w) return sliceVisible(s, w);
1024
+ return s + " ".repeat(w - vis);
1025
+ }
967
1026
  function wrapTextLines(text, cols) {
968
1027
  const w = Math.max(1, Math.floor(Number(cols) || 1));
969
1028
  const out = [];
@@ -972,20 +1031,23 @@ function wrapTextLines(text, cols) {
972
1031
  out.push("");
973
1032
  continue;
974
1033
  }
1034
+ if (visibleWidth(raw) <= w) {
1035
+ out.push(raw);
1036
+ continue;
1037
+ }
975
1038
  let rest = raw;
976
- while (rest.length > w) {
977
- out.push(rest.slice(0, w));
978
- rest = rest.slice(w);
1039
+ while (visibleWidth(rest) > w) {
1040
+ const head = sliceVisible(rest, w);
1041
+ out.push(head);
1042
+ rest = rest.slice(head.length);
979
1043
  }
980
1044
  if (rest.length > 0) out.push(rest);
981
1045
  }
982
1046
  return out;
983
1047
  }
984
- function padEndVisible(s, w) {
985
- const t = String(s ?? "");
986
- if (t.length >= w) return t.slice(0, w);
987
- return t + " ".repeat(w - t.length);
988
- }
1048
+
1049
+ // src/screen/scrollable-text.js
1050
+ var h5 = createElement2;
989
1051
  var SCROLL_KEYS = [
990
1052
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
991
1053
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
@@ -1000,9 +1062,11 @@ function ScrollableText({
1000
1062
  showScrollbar = true,
1001
1063
  showStatus = true,
1002
1064
  bindKeys = true,
1003
- header = null
1065
+ header = null,
1066
+ followBottom = false
1004
1067
  }) {
1005
1068
  const [scrollTop, setScrollTop] = useState3(0);
1069
+ const [following, setFollowing] = useState3(() => !!followBottom);
1006
1070
  const [, bump] = useState3(0);
1007
1071
  const termRows = process.stdout.rows || 24;
1008
1072
  const viewportRows = Math.max(
@@ -1021,48 +1085,52 @@ function ScrollableText({
1021
1085
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1022
1086
  const visible = allLines.slice(clamped, clamped + viewportRows);
1023
1087
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1024
- useEffect2(() => {
1025
- setScrollTop((s) => Math.min(s, maxScroll));
1026
- }, [maxScroll]);
1027
1088
  const maxScrollRef = useRef2(maxScroll);
1028
1089
  const pageSizeRef = useRef2(viewportRows);
1090
+ const scrollTopRef = useRef2(scrollTop);
1091
+ const followingRef = useRef2(following);
1029
1092
  maxScrollRef.current = maxScroll;
1030
1093
  pageSizeRef.current = viewportRows;
1094
+ scrollTopRef.current = scrollTop;
1095
+ followingRef.current = following;
1096
+ useEffect2(() => {
1097
+ const next = nextScrollAfterContentChange({
1098
+ following: followBottom && followingRef.current,
1099
+ scrollTop: scrollTopRef.current,
1100
+ maxScroll
1101
+ });
1102
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1103
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1104
+ }, [maxScroll, followBottom]);
1105
+ const applyUserScroll = (delta) => {
1106
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1107
+ setScrollTop(next.scrollTop);
1108
+ if (followBottom) setFollowing(next.following);
1109
+ bump((n) => n + 1);
1110
+ ctx?.update?.();
1111
+ };
1031
1112
  useEffect2(() => {
1032
1113
  if (!ctx || !bindKeys) return void 0;
1033
1114
  ctx.setKeyBinding(SCROLL_KEYS);
1034
- ctx.setAction("scrollUp", () => {
1035
- setScrollTop((s) => Math.max(0, s - 1));
1036
- bump((n) => n + 1);
1037
- ctx.update?.();
1038
- });
1039
- ctx.setAction("scrollDown", () => {
1040
- setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1041
- bump((n) => n + 1);
1042
- ctx.update?.();
1043
- });
1044
- ctx.setAction("pageUp", () => {
1045
- setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1046
- bump((n) => n + 1);
1047
- ctx.update?.();
1048
- });
1049
- ctx.setAction("pageDown", () => {
1050
- setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1051
- bump((n) => n + 1);
1052
- ctx.update?.();
1053
- });
1115
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1116
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1117
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1118
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1054
1119
  return void 0;
1055
- }, [ctx, bindKeys]);
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" : "");
1120
+ }, [ctx, bindKeys, followBottom]);
1121
+ 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" : "") + (followBottom && following ? " \xB7 follow" : followBottom ? " \xB7 follow off" : "");
1057
1122
  const rowNodes = visible.map((line, i) => {
1058
- const body = padEndVisible(line, textWidth);
1059
1123
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1060
1124
  const isThumb = glyph === BAR_THUMB;
1061
1125
  return h5(
1062
- Text5,
1063
- { key: `L${clamped + i}` },
1064
- body,
1065
- glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1126
+ Box4,
1127
+ { key: `L${clamped + i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1128
+ h5(
1129
+ Box4,
1130
+ { width: textWidth, flexShrink: 0 },
1131
+ h5(Text5, { wrap: "truncate" }, padEndVisible(line, textWidth))
1132
+ ),
1133
+ showScrollbar ? h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph)) : null
1066
1134
  );
1067
1135
  });
1068
1136
  if (bar && visible.length < viewportRows) {
@@ -1070,10 +1138,10 @@ function ScrollableText({
1070
1138
  const glyph = bar[i] ?? BAR_TRACK;
1071
1139
  rowNodes.push(
1072
1140
  h5(
1073
- Text5,
1074
- { key: `pad${i}` },
1075
- padEndVisible("", textWidth),
1076
- h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1141
+ Box4,
1142
+ { key: `pad${i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1143
+ h5(Box4, { width: textWidth, flexShrink: 0 }, h5(Text5, {}, padEndVisible("", textWidth))),
1144
+ h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph))
1077
1145
  )
1078
1146
  );
1079
1147
  }
@@ -1249,11 +1317,16 @@ export {
1249
1317
  buildBreadcrumb,
1250
1318
  buildDetailBreadcrumb,
1251
1319
  buildFooter,
1320
+ clampScroll,
1252
1321
  formatBindingKey,
1253
1322
  createElement3 as h,
1323
+ isScrolledToBottom,
1254
1324
  load,
1255
1325
  memo,
1326
+ nextScrollAfterContentChange,
1327
+ nextScrollAfterUserMove,
1256
1328
  organizeFooterMessages,
1329
+ padEndVisible,
1257
1330
  scrollbarGlyphs,
1258
1331
  showListScreen,
1259
1332
  showMenuScreen,
@@ -1261,6 +1334,8 @@ export {
1261
1334
  showMultiColumnListWithPreviewScreen,
1262
1335
  showScreen,
1263
1336
  showWordGridScreen,
1337
+ sliceVisible,
1338
+ stripAnsi,
1264
1339
  useCallback,
1265
1340
  useEffect3 as useEffect,
1266
1341
  useInput2 as useInput,
@@ -1268,6 +1343,7 @@ export {
1268
1343
  useMemo2 as useMemo,
1269
1344
  useRef3 as useRef,
1270
1345
  useState4 as useState,
1346
+ visibleWidth,
1271
1347
  wrapTextLines
1272
1348
  };
1273
1349
  //# sourceMappingURL=screen.js.map