@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.cjs CHANGED
@@ -56,11 +56,16 @@ __export(screen_exports, {
56
56
  buildBreadcrumb: () => buildBreadcrumb,
57
57
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
58
58
  buildFooter: () => buildFooter,
59
+ clampScroll: () => clampScroll,
59
60
  formatBindingKey: () => formatBindingKey,
60
61
  h: () => import_react6.createElement,
62
+ isScrolledToBottom: () => isScrolledToBottom,
61
63
  load: () => load,
62
64
  memo: () => import_react6.memo,
65
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
66
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
63
67
  organizeFooterMessages: () => organizeFooterMessages,
68
+ padEndVisible: () => padEndVisible,
64
69
  scrollbarGlyphs: () => scrollbarGlyphs,
65
70
  showListScreen: () => showListScreen,
66
71
  showMenuScreen: () => showMenuScreen,
@@ -68,6 +73,8 @@ __export(screen_exports, {
68
73
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
69
74
  showScreen: () => showScreen,
70
75
  showWordGridScreen: () => showWordGridScreen,
76
+ sliceVisible: () => sliceVisible,
77
+ stripAnsi: () => stripAnsi,
71
78
  useCallback: () => import_react6.useCallback,
72
79
  useEffect: () => import_react6.useEffect,
73
80
  useInput: () => import_ink6.useInput,
@@ -75,6 +82,7 @@ __export(screen_exports, {
75
82
  useMemo: () => import_react6.useMemo,
76
83
  useRef: () => import_react6.useRef,
77
84
  useState: () => import_react6.useState,
85
+ visibleWidth: () => visibleWidth,
78
86
  wrapTextLines: () => wrapTextLines
79
87
  });
80
88
  module.exports = __toCommonJS(screen_exports);
@@ -1042,7 +1050,66 @@ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit })
1042
1050
  // src/screen/scrollable-text.js
1043
1051
  var import_react5 = null; // Will be set by load();
1044
1052
  var import_ink5 = null; // Will be set by load();
1045
- // Removed: intermediate assignment - code will access import variable directly
1053
+
1054
+ // src/screen/follow-scroll.js
1055
+ function clampScroll(scrollTop, maxScroll) {
1056
+ const max = Math.max(0, Number(maxScroll) || 0);
1057
+ const top = Number(scrollTop) || 0;
1058
+ return Math.min(Math.max(0, top), max);
1059
+ }
1060
+ function isScrolledToBottom(scrollTop, maxScroll) {
1061
+ return clampScroll(scrollTop, maxScroll) >= Math.max(0, Number(maxScroll) || 0);
1062
+ }
1063
+ function nextScrollAfterUserMove(scrollTop, maxScroll, delta) {
1064
+ const next = clampScroll((Number(scrollTop) || 0) + (Number(delta) || 0), maxScroll);
1065
+ return { scrollTop: next, following: isScrolledToBottom(next, maxScroll) };
1066
+ }
1067
+ function nextScrollAfterContentChange({ following, scrollTop, maxScroll }) {
1068
+ const max = Math.max(0, Number(maxScroll) || 0);
1069
+ if (following) return { scrollTop: max, following: true };
1070
+ const next = clampScroll(scrollTop, max);
1071
+ return { scrollTop: next, following: isScrolledToBottom(next, max) };
1072
+ }
1073
+
1074
+ // src/screen/visible-text.js
1075
+ var ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1076
+ function stripAnsi(text) {
1077
+ return String(text ?? "").replace(ANSI_RE, "");
1078
+ }
1079
+ function visibleWidth(text) {
1080
+ return [...stripAnsi(text)].length;
1081
+ }
1082
+ function sliceVisible(text, cols) {
1083
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1084
+ const s = String(text ?? "");
1085
+ if (w === 0) return "";
1086
+ let vis = 0;
1087
+ let out = "";
1088
+ let i = 0;
1089
+ while (i < s.length && vis < w) {
1090
+ ANSI_RE.lastIndex = i;
1091
+ const m = ANSI_RE.exec(s);
1092
+ if (m && m.index === i) {
1093
+ out += m[0];
1094
+ i += m[0].length;
1095
+ continue;
1096
+ }
1097
+ const cp = s.codePointAt(i);
1098
+ const ch = String.fromCodePoint(cp);
1099
+ out += ch;
1100
+ i += ch.length;
1101
+ vis += 1;
1102
+ }
1103
+ return out;
1104
+ }
1105
+ function padEndVisible(text, cols) {
1106
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1107
+ const s = String(text ?? "");
1108
+ const vis = visibleWidth(s);
1109
+ if (vis === w) return s;
1110
+ if (vis > w) return sliceVisible(s, w);
1111
+ return s + " ".repeat(w - vis);
1112
+ }
1046
1113
  function wrapTextLines(text, cols) {
1047
1114
  const w = Math.max(1, Math.floor(Number(cols) || 1));
1048
1115
  const out = [];
@@ -1051,20 +1118,23 @@ function wrapTextLines(text, cols) {
1051
1118
  out.push("");
1052
1119
  continue;
1053
1120
  }
1121
+ if (visibleWidth(raw) <= w) {
1122
+ out.push(raw);
1123
+ continue;
1124
+ }
1054
1125
  let rest = raw;
1055
- while (rest.length > w) {
1056
- out.push(rest.slice(0, w));
1057
- rest = rest.slice(w);
1126
+ while (visibleWidth(rest) > w) {
1127
+ const head = sliceVisible(rest, w);
1128
+ out.push(head);
1129
+ rest = rest.slice(head.length);
1058
1130
  }
1059
1131
  if (rest.length > 0) out.push(rest);
1060
1132
  }
1061
1133
  return out;
1062
1134
  }
1063
- function padEndVisible(s, w) {
1064
- const t = String(s ?? "");
1065
- if (t.length >= w) return t.slice(0, w);
1066
- return t + " ".repeat(w - t.length);
1067
- }
1135
+
1136
+ // src/screen/scrollable-text.js
1137
+ // Removed: intermediate assignment - code will access import variable directly
1068
1138
  var SCROLL_KEYS = [
1069
1139
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1070
1140
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
@@ -1079,9 +1149,11 @@ function ScrollableText({
1079
1149
  showScrollbar = true,
1080
1150
  showStatus = true,
1081
1151
  bindKeys = true,
1082
- header = null
1152
+ header = null,
1153
+ followBottom = false
1083
1154
  }) {
1084
1155
  const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
1156
+ const [following, setFollowing] = (0, import_react5.useState)(() => !!followBottom);
1085
1157
  const [, bump] = (0, import_react5.useState)(0);
1086
1158
  const termRows = process.stdout.rows || 24;
1087
1159
  const viewportRows = Math.max(
@@ -1100,48 +1172,52 @@ function ScrollableText({
1100
1172
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1101
1173
  const visible = allLines.slice(clamped, clamped + viewportRows);
1102
1174
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1103
- (0, import_react5.useEffect)(() => {
1104
- setScrollTop((s) => Math.min(s, maxScroll));
1105
- }, [maxScroll]);
1106
1175
  const maxScrollRef = (0, import_react5.useRef)(maxScroll);
1107
1176
  const pageSizeRef = (0, import_react5.useRef)(viewportRows);
1177
+ const scrollTopRef = (0, import_react5.useRef)(scrollTop);
1178
+ const followingRef = (0, import_react5.useRef)(following);
1108
1179
  maxScrollRef.current = maxScroll;
1109
1180
  pageSizeRef.current = viewportRows;
1181
+ scrollTopRef.current = scrollTop;
1182
+ followingRef.current = following;
1183
+ (0, import_react5.useEffect)(() => {
1184
+ const next = nextScrollAfterContentChange({
1185
+ following: followBottom && followingRef.current,
1186
+ scrollTop: scrollTopRef.current,
1187
+ maxScroll
1188
+ });
1189
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1190
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1191
+ }, [maxScroll, followBottom]);
1192
+ const applyUserScroll = (delta) => {
1193
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1194
+ setScrollTop(next.scrollTop);
1195
+ if (followBottom) setFollowing(next.following);
1196
+ bump((n) => n + 1);
1197
+ ctx?.update?.();
1198
+ };
1110
1199
  (0, import_react5.useEffect)(() => {
1111
1200
  if (!ctx || !bindKeys) return void 0;
1112
1201
  ctx.setKeyBinding(SCROLL_KEYS);
1113
- ctx.setAction("scrollUp", () => {
1114
- setScrollTop((s) => Math.max(0, s - 1));
1115
- bump((n) => n + 1);
1116
- ctx.update?.();
1117
- });
1118
- ctx.setAction("scrollDown", () => {
1119
- setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1120
- bump((n) => n + 1);
1121
- ctx.update?.();
1122
- });
1123
- ctx.setAction("pageUp", () => {
1124
- setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1125
- bump((n) => n + 1);
1126
- ctx.update?.();
1127
- });
1128
- ctx.setAction("pageDown", () => {
1129
- setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1130
- bump((n) => n + 1);
1131
- ctx.update?.();
1132
- });
1202
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1203
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1204
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1205
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1133
1206
  return void 0;
1134
- }, [ctx, bindKeys]);
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" : "");
1207
+ }, [ctx, bindKeys, followBottom]);
1208
+ 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" : "");
1136
1209
  const rowNodes = visible.map((line, i) => {
1137
- const body = padEndVisible(line, textWidth);
1138
1210
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1139
1211
  const isThumb = glyph === BAR_THUMB;
1140
1212
  return h5(
1141
- import_ink5.Text,
1142
- { key: `L${clamped + i}` },
1143
- body,
1144
- glyph ? h5(import_ink5.Text, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1213
+ import_ink5.Box,
1214
+ { key: `L${clamped + i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1215
+ h5(
1216
+ import_ink5.Box,
1217
+ { width: textWidth, flexShrink: 0 },
1218
+ h5(import_ink5.Text, { wrap: "truncate" }, padEndVisible(line, textWidth))
1219
+ ),
1220
+ showScrollbar ? h5(import_ink5.Box, { width: 1, flexShrink: 0 }, h5(import_ink5.Text, { color: isThumb ? "cyan" : "gray" }, glyph)) : null
1145
1221
  );
1146
1222
  });
1147
1223
  if (bar && visible.length < viewportRows) {
@@ -1149,10 +1225,10 @@ function ScrollableText({
1149
1225
  const glyph = bar[i] ?? BAR_TRACK;
1150
1226
  rowNodes.push(
1151
1227
  h5(
1152
- import_ink5.Text,
1153
- { key: `pad${i}` },
1154
- padEndVisible("", textWidth),
1155
- h5(import_ink5.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1228
+ import_ink5.Box,
1229
+ { key: `pad${i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1230
+ h5(import_ink5.Box, { width: textWidth, flexShrink: 0 }, h5(import_ink5.Text, {}, padEndVisible("", textWidth))),
1231
+ h5(import_ink5.Box, { width: 1, flexShrink: 0 }, h5(import_ink5.Text, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph))
1156
1232
  )
1157
1233
  );
1158
1234
  }
@@ -1341,11 +1417,16 @@ if (typeof window === "undefined") {
1341
1417
  buildBreadcrumb,
1342
1418
  buildDetailBreadcrumb,
1343
1419
  buildFooter,
1420
+ clampScroll,
1344
1421
  formatBindingKey,
1345
1422
  h,
1423
+ isScrolledToBottom,
1346
1424
  load,
1347
1425
  memo,
1426
+ nextScrollAfterContentChange,
1427
+ nextScrollAfterUserMove,
1348
1428
  organizeFooterMessages,
1429
+ padEndVisible,
1349
1430
  scrollbarGlyphs,
1350
1431
  showListScreen,
1351
1432
  showMenuScreen,
@@ -1353,6 +1434,8 @@ if (typeof window === "undefined") {
1353
1434
  showMultiColumnListWithPreviewScreen,
1354
1435
  showScreen,
1355
1436
  showWordGridScreen,
1437
+ sliceVisible,
1438
+ stripAnsi,
1356
1439
  useCallback,
1357
1440
  useEffect,
1358
1441
  useInput,
@@ -1360,6 +1443,7 @@ if (typeof window === "undefined") {
1360
1443
  useMemo,
1361
1444
  useRef,
1362
1445
  useState,
1446
+ visibleWidth,
1363
1447
  wrapTextLines
1364
1448
  });
1365
1449
  //# sourceMappingURL=screen.cjs.map