@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.
@@ -1018,7 +1018,68 @@ var init_ui_elements = __esm({
1018
1018
  }
1019
1019
  });
1020
1020
 
1021
- // src/screen/scrollable-text.js
1021
+ // src/screen/follow-scroll.js
1022
+ function clampScroll(scrollTop, maxScroll) {
1023
+ const max = Math.max(0, Number(maxScroll) || 0);
1024
+ const top = Number(scrollTop) || 0;
1025
+ return Math.min(Math.max(0, top), max);
1026
+ }
1027
+ function isScrolledToBottom(scrollTop, maxScroll) {
1028
+ return clampScroll(scrollTop, maxScroll) >= Math.max(0, Number(maxScroll) || 0);
1029
+ }
1030
+ function nextScrollAfterUserMove(scrollTop, maxScroll, delta) {
1031
+ const next = clampScroll((Number(scrollTop) || 0) + (Number(delta) || 0), maxScroll);
1032
+ return { scrollTop: next, following: isScrolledToBottom(next, maxScroll) };
1033
+ }
1034
+ function nextScrollAfterContentChange({ following, scrollTop, maxScroll }) {
1035
+ const max = Math.max(0, Number(maxScroll) || 0);
1036
+ if (following) return { scrollTop: max, following: true };
1037
+ const next = clampScroll(scrollTop, max);
1038
+ return { scrollTop: next, following: isScrolledToBottom(next, max) };
1039
+ }
1040
+ var init_follow_scroll = __esm({
1041
+ "src/screen/follow-scroll.js"() {
1042
+ }
1043
+ });
1044
+
1045
+ // src/screen/visible-text.js
1046
+ function stripAnsi(text) {
1047
+ return String(text ?? "").replace(ANSI_RE, "");
1048
+ }
1049
+ function visibleWidth(text) {
1050
+ return [...stripAnsi(text)].length;
1051
+ }
1052
+ function sliceVisible(text, cols) {
1053
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1054
+ const s = String(text ?? "");
1055
+ if (w === 0) return "";
1056
+ let vis = 0;
1057
+ let out = "";
1058
+ let i = 0;
1059
+ while (i < s.length && vis < w) {
1060
+ ANSI_RE.lastIndex = i;
1061
+ const m = ANSI_RE.exec(s);
1062
+ if (m && m.index === i) {
1063
+ out += m[0];
1064
+ i += m[0].length;
1065
+ continue;
1066
+ }
1067
+ const cp = s.codePointAt(i);
1068
+ const ch = String.fromCodePoint(cp);
1069
+ out += ch;
1070
+ i += ch.length;
1071
+ vis += 1;
1072
+ }
1073
+ return out;
1074
+ }
1075
+ function padEndVisible(text, cols) {
1076
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1077
+ const s = String(text ?? "");
1078
+ const vis = visibleWidth(s);
1079
+ if (vis === w) return s;
1080
+ if (vis > w) return sliceVisible(s, w);
1081
+ return s + " ".repeat(w - vis);
1082
+ }
1022
1083
  function wrapTextLines(text, cols) {
1023
1084
  const w = Math.max(1, Math.floor(Number(cols) || 1));
1024
1085
  const out = [];
@@ -1027,20 +1088,28 @@ function wrapTextLines(text, cols) {
1027
1088
  out.push("");
1028
1089
  continue;
1029
1090
  }
1091
+ if (visibleWidth(raw) <= w) {
1092
+ out.push(raw);
1093
+ continue;
1094
+ }
1030
1095
  let rest = raw;
1031
- while (rest.length > w) {
1032
- out.push(rest.slice(0, w));
1033
- rest = rest.slice(w);
1096
+ while (visibleWidth(rest) > w) {
1097
+ const head = sliceVisible(rest, w);
1098
+ out.push(head);
1099
+ rest = rest.slice(head.length);
1034
1100
  }
1035
1101
  if (rest.length > 0) out.push(rest);
1036
1102
  }
1037
1103
  return out;
1038
1104
  }
1039
- function padEndVisible(s, w) {
1040
- const t = String(s ?? "");
1041
- if (t.length >= w) return t.slice(0, w);
1042
- return t + " ".repeat(w - t.length);
1043
- }
1105
+ var ANSI_RE;
1106
+ var init_visible_text = __esm({
1107
+ "src/screen/visible-text.js"() {
1108
+ ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1109
+ }
1110
+ });
1111
+
1112
+ // src/screen/scrollable-text.js
1044
1113
  function ScrollableText({
1045
1114
  ctx,
1046
1115
  text = "",
@@ -1050,9 +1119,11 @@ function ScrollableText({
1050
1119
  showScrollbar = true,
1051
1120
  showStatus = true,
1052
1121
  bindKeys = true,
1053
- header = null
1122
+ header = null,
1123
+ followBottom = false
1054
1124
  }) {
1055
1125
  const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
1126
+ const [following, setFollowing] = (0, import_react5.useState)(() => !!followBottom);
1056
1127
  const [, bump] = (0, import_react5.useState)(0);
1057
1128
  const termRows = process.stdout.rows || 24;
1058
1129
  const viewportRows = Math.max(
@@ -1071,48 +1142,52 @@ function ScrollableText({
1071
1142
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1072
1143
  const visible = allLines.slice(clamped, clamped + viewportRows);
1073
1144
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1074
- (0, import_react5.useEffect)(() => {
1075
- setScrollTop((s) => Math.min(s, maxScroll));
1076
- }, [maxScroll]);
1077
1145
  const maxScrollRef = (0, import_react5.useRef)(maxScroll);
1078
1146
  const pageSizeRef = (0, import_react5.useRef)(viewportRows);
1147
+ const scrollTopRef = (0, import_react5.useRef)(scrollTop);
1148
+ const followingRef = (0, import_react5.useRef)(following);
1079
1149
  maxScrollRef.current = maxScroll;
1080
1150
  pageSizeRef.current = viewportRows;
1151
+ scrollTopRef.current = scrollTop;
1152
+ followingRef.current = following;
1153
+ (0, import_react5.useEffect)(() => {
1154
+ const next = nextScrollAfterContentChange({
1155
+ following: followBottom && followingRef.current,
1156
+ scrollTop: scrollTopRef.current,
1157
+ maxScroll
1158
+ });
1159
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1160
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1161
+ }, [maxScroll, followBottom]);
1162
+ const applyUserScroll = (delta) => {
1163
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1164
+ setScrollTop(next.scrollTop);
1165
+ if (followBottom) setFollowing(next.following);
1166
+ bump((n) => n + 1);
1167
+ ctx?.update?.();
1168
+ };
1081
1169
  (0, import_react5.useEffect)(() => {
1082
1170
  if (!ctx || !bindKeys) return void 0;
1083
1171
  ctx.setKeyBinding(SCROLL_KEYS);
1084
- ctx.setAction("scrollUp", () => {
1085
- setScrollTop((s) => Math.max(0, s - 1));
1086
- bump((n) => n + 1);
1087
- ctx.update?.();
1088
- });
1089
- ctx.setAction("scrollDown", () => {
1090
- setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1091
- bump((n) => n + 1);
1092
- ctx.update?.();
1093
- });
1094
- ctx.setAction("pageUp", () => {
1095
- setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1096
- bump((n) => n + 1);
1097
- ctx.update?.();
1098
- });
1099
- ctx.setAction("pageDown", () => {
1100
- setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1101
- bump((n) => n + 1);
1102
- ctx.update?.();
1103
- });
1172
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1173
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1174
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1175
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1104
1176
  return void 0;
1105
- }, [ctx, bindKeys]);
1106
- 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" : "");
1177
+ }, [ctx, bindKeys, followBottom]);
1178
+ 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" : "");
1107
1179
  const rowNodes = visible.map((line, i) => {
1108
- const body = padEndVisible(line, textWidth);
1109
1180
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1110
1181
  const isThumb = glyph === BAR_THUMB;
1111
1182
  return h5(
1112
- import_ink5.Text,
1113
- { key: `L${clamped + i}` },
1114
- body,
1115
- glyph ? h5(import_ink5.Text, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1183
+ import_ink5.Box,
1184
+ { key: `L${clamped + i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1185
+ h5(
1186
+ import_ink5.Box,
1187
+ { width: textWidth, flexShrink: 0 },
1188
+ h5(import_ink5.Text, { wrap: "truncate" }, padEndVisible(line, textWidth))
1189
+ ),
1190
+ showScrollbar ? h5(import_ink5.Box, { width: 1, flexShrink: 0 }, h5(import_ink5.Text, { color: isThumb ? "cyan" : "gray" }, glyph)) : null
1116
1191
  );
1117
1192
  });
1118
1193
  if (bar && visible.length < viewportRows) {
@@ -1120,10 +1195,10 @@ function ScrollableText({
1120
1195
  const glyph = bar[i] ?? BAR_TRACK;
1121
1196
  rowNodes.push(
1122
1197
  h5(
1123
- import_ink5.Text,
1124
- { key: `pad${i}` },
1125
- padEndVisible("", textWidth),
1126
- h5(import_ink5.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1198
+ import_ink5.Box,
1199
+ { key: `pad${i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1200
+ h5(import_ink5.Box, { width: textWidth, flexShrink: 0 }, h5(import_ink5.Text, {}, padEndVisible("", textWidth))),
1201
+ h5(import_ink5.Box, { width: 1, flexShrink: 0 }, h5(import_ink5.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph))
1127
1202
  )
1128
1203
  );
1129
1204
  }
@@ -1143,7 +1218,10 @@ var init_scrollable_text = __esm({
1143
1218
  import_ink5 = require("ink");
1144
1219
  init_components();
1145
1220
  init_scrollbar();
1221
+ init_follow_scroll();
1222
+ init_visible_text();
1146
1223
  init_scrollbar();
1224
+ init_visible_text();
1147
1225
  h5 = import_react5.createElement;
1148
1226
  SCROLL_KEYS = [
1149
1227
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
@@ -1311,11 +1389,16 @@ __export(screen_exports, {
1311
1389
  buildBreadcrumb: () => buildBreadcrumb,
1312
1390
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1313
1391
  buildFooter: () => buildFooter,
1392
+ clampScroll: () => clampScroll,
1314
1393
  formatBindingKey: () => formatBindingKey,
1315
1394
  h: () => import_react6.createElement,
1395
+ isScrolledToBottom: () => isScrolledToBottom,
1316
1396
  load: () => load,
1317
1397
  memo: () => import_react6.memo,
1398
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
1399
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
1318
1400
  organizeFooterMessages: () => organizeFooterMessages,
1401
+ padEndVisible: () => padEndVisible,
1319
1402
  scrollbarGlyphs: () => scrollbarGlyphs,
1320
1403
  showListScreen: () => showListScreen,
1321
1404
  showMenuScreen: () => showMenuScreen,
@@ -1323,6 +1406,8 @@ __export(screen_exports, {
1323
1406
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1324
1407
  showScreen: () => showScreen,
1325
1408
  showWordGridScreen: () => showWordGridScreen,
1409
+ sliceVisible: () => sliceVisible,
1410
+ stripAnsi: () => stripAnsi,
1326
1411
  useCallback: () => import_react6.useCallback,
1327
1412
  useEffect: () => import_react6.useEffect,
1328
1413
  useInput: () => import_ink6.useInput,
@@ -1330,6 +1415,7 @@ __export(screen_exports, {
1330
1415
  useMemo: () => import_react6.useMemo,
1331
1416
  useRef: () => import_react6.useRef,
1332
1417
  useState: () => import_react6.useState,
1418
+ visibleWidth: () => visibleWidth,
1333
1419
  wrapTextLines: () => wrapTextLines
1334
1420
  });
1335
1421
  async function load() {
@@ -1351,6 +1437,8 @@ var init_screen = __esm({
1351
1437
  init_components();
1352
1438
  init_ui_elements();
1353
1439
  init_scrollable_text();
1440
+ init_visible_text();
1441
+ init_follow_scroll();
1354
1442
  init_scrollbar();
1355
1443
  init_key_bindings();
1356
1444
  init_utils();