@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/index.js CHANGED
@@ -996,9 +996,68 @@ var init_ui_elements = __esm({
996
996
  }
997
997
  });
998
998
 
999
- // src/screen/scrollable-text.js
1000
- import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
1001
- import { Box as Box4, Text as Text5 } from "ink";
999
+ // src/screen/follow-scroll.js
1000
+ function clampScroll(scrollTop, maxScroll) {
1001
+ const max = Math.max(0, Number(maxScroll) || 0);
1002
+ const top = Number(scrollTop) || 0;
1003
+ return Math.min(Math.max(0, top), max);
1004
+ }
1005
+ function isScrolledToBottom(scrollTop, maxScroll) {
1006
+ return clampScroll(scrollTop, maxScroll) >= Math.max(0, Number(maxScroll) || 0);
1007
+ }
1008
+ function nextScrollAfterUserMove(scrollTop, maxScroll, delta) {
1009
+ const next = clampScroll((Number(scrollTop) || 0) + (Number(delta) || 0), maxScroll);
1010
+ return { scrollTop: next, following: isScrolledToBottom(next, maxScroll) };
1011
+ }
1012
+ function nextScrollAfterContentChange({ following, scrollTop, maxScroll }) {
1013
+ const max = Math.max(0, Number(maxScroll) || 0);
1014
+ if (following) return { scrollTop: max, following: true };
1015
+ const next = clampScroll(scrollTop, max);
1016
+ return { scrollTop: next, following: isScrolledToBottom(next, max) };
1017
+ }
1018
+ var init_follow_scroll = __esm({
1019
+ "src/screen/follow-scroll.js"() {
1020
+ }
1021
+ });
1022
+
1023
+ // src/screen/visible-text.js
1024
+ function stripAnsi(text) {
1025
+ return String(text ?? "").replace(ANSI_RE, "");
1026
+ }
1027
+ function visibleWidth(text) {
1028
+ return [...stripAnsi(text)].length;
1029
+ }
1030
+ function sliceVisible(text, cols) {
1031
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1032
+ const s = String(text ?? "");
1033
+ if (w === 0) return "";
1034
+ let vis = 0;
1035
+ let out = "";
1036
+ let i = 0;
1037
+ while (i < s.length && vis < w) {
1038
+ ANSI_RE.lastIndex = i;
1039
+ const m = ANSI_RE.exec(s);
1040
+ if (m && m.index === i) {
1041
+ out += m[0];
1042
+ i += m[0].length;
1043
+ continue;
1044
+ }
1045
+ const cp3 = s.codePointAt(i);
1046
+ const ch = String.fromCodePoint(cp3);
1047
+ out += ch;
1048
+ i += ch.length;
1049
+ vis += 1;
1050
+ }
1051
+ return out;
1052
+ }
1053
+ function padEndVisible(text, cols) {
1054
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1055
+ const s = String(text ?? "");
1056
+ const vis = visibleWidth(s);
1057
+ if (vis === w) return s;
1058
+ if (vis > w) return sliceVisible(s, w);
1059
+ return s + " ".repeat(w - vis);
1060
+ }
1002
1061
  function wrapTextLines(text, cols) {
1003
1062
  const w = Math.max(1, Math.floor(Number(cols) || 1));
1004
1063
  const out = [];
@@ -1007,20 +1066,30 @@ function wrapTextLines(text, cols) {
1007
1066
  out.push("");
1008
1067
  continue;
1009
1068
  }
1069
+ if (visibleWidth(raw) <= w) {
1070
+ out.push(raw);
1071
+ continue;
1072
+ }
1010
1073
  let rest = raw;
1011
- while (rest.length > w) {
1012
- out.push(rest.slice(0, w));
1013
- rest = rest.slice(w);
1074
+ while (visibleWidth(rest) > w) {
1075
+ const head = sliceVisible(rest, w);
1076
+ out.push(head);
1077
+ rest = rest.slice(head.length);
1014
1078
  }
1015
1079
  if (rest.length > 0) out.push(rest);
1016
1080
  }
1017
1081
  return out;
1018
1082
  }
1019
- function padEndVisible(s, w) {
1020
- const t = String(s ?? "");
1021
- if (t.length >= w) return t.slice(0, w);
1022
- return t + " ".repeat(w - t.length);
1023
- }
1083
+ var ANSI_RE;
1084
+ var init_visible_text = __esm({
1085
+ "src/screen/visible-text.js"() {
1086
+ ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1087
+ }
1088
+ });
1089
+
1090
+ // src/screen/scrollable-text.js
1091
+ import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
1092
+ import { Box as Box4, Text as Text5 } from "ink";
1024
1093
  function ScrollableText({
1025
1094
  ctx,
1026
1095
  text = "",
@@ -1030,9 +1099,11 @@ function ScrollableText({
1030
1099
  showScrollbar = true,
1031
1100
  showStatus = true,
1032
1101
  bindKeys = true,
1033
- header = null
1102
+ header = null,
1103
+ followBottom = false
1034
1104
  }) {
1035
1105
  const [scrollTop, setScrollTop] = useState3(0);
1106
+ const [following, setFollowing] = useState3(() => !!followBottom);
1036
1107
  const [, bump] = useState3(0);
1037
1108
  const termRows = process.stdout.rows || 24;
1038
1109
  const viewportRows = Math.max(
@@ -1051,48 +1122,52 @@ function ScrollableText({
1051
1122
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1052
1123
  const visible = allLines.slice(clamped, clamped + viewportRows);
1053
1124
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1054
- useEffect2(() => {
1055
- setScrollTop((s) => Math.min(s, maxScroll));
1056
- }, [maxScroll]);
1057
1125
  const maxScrollRef = useRef2(maxScroll);
1058
1126
  const pageSizeRef = useRef2(viewportRows);
1127
+ const scrollTopRef = useRef2(scrollTop);
1128
+ const followingRef = useRef2(following);
1059
1129
  maxScrollRef.current = maxScroll;
1060
1130
  pageSizeRef.current = viewportRows;
1131
+ scrollTopRef.current = scrollTop;
1132
+ followingRef.current = following;
1133
+ useEffect2(() => {
1134
+ const next = nextScrollAfterContentChange({
1135
+ following: followBottom && followingRef.current,
1136
+ scrollTop: scrollTopRef.current,
1137
+ maxScroll
1138
+ });
1139
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1140
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1141
+ }, [maxScroll, followBottom]);
1142
+ const applyUserScroll = (delta) => {
1143
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1144
+ setScrollTop(next.scrollTop);
1145
+ if (followBottom) setFollowing(next.following);
1146
+ bump((n) => n + 1);
1147
+ ctx?.update?.();
1148
+ };
1061
1149
  useEffect2(() => {
1062
1150
  if (!ctx || !bindKeys) return void 0;
1063
1151
  ctx.setKeyBinding(SCROLL_KEYS);
1064
- ctx.setAction("scrollUp", () => {
1065
- setScrollTop((s) => Math.max(0, s - 1));
1066
- bump((n) => n + 1);
1067
- ctx.update?.();
1068
- });
1069
- ctx.setAction("scrollDown", () => {
1070
- setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1071
- bump((n) => n + 1);
1072
- ctx.update?.();
1073
- });
1074
- ctx.setAction("pageUp", () => {
1075
- setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1076
- bump((n) => n + 1);
1077
- ctx.update?.();
1078
- });
1079
- ctx.setAction("pageDown", () => {
1080
- setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1081
- bump((n) => n + 1);
1082
- ctx.update?.();
1083
- });
1152
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1153
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1154
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1155
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1084
1156
  return void 0;
1085
- }, [ctx, bindKeys]);
1086
- 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" : "");
1157
+ }, [ctx, bindKeys, followBottom]);
1158
+ 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" : "");
1087
1159
  const rowNodes = visible.map((line, i) => {
1088
- const body = padEndVisible(line, textWidth);
1089
1160
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1090
1161
  const isThumb = glyph === BAR_THUMB;
1091
1162
  return h5(
1092
- Text5,
1093
- { key: `L${clamped + i}` },
1094
- body,
1095
- glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1163
+ Box4,
1164
+ { key: `L${clamped + i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1165
+ h5(
1166
+ Box4,
1167
+ { width: textWidth, flexShrink: 0 },
1168
+ h5(Text5, { wrap: "truncate" }, padEndVisible(line, textWidth))
1169
+ ),
1170
+ showScrollbar ? h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph)) : null
1096
1171
  );
1097
1172
  });
1098
1173
  if (bar && visible.length < viewportRows) {
@@ -1100,10 +1175,10 @@ function ScrollableText({
1100
1175
  const glyph = bar[i] ?? BAR_TRACK;
1101
1176
  rowNodes.push(
1102
1177
  h5(
1103
- Text5,
1104
- { key: `pad${i}` },
1105
- padEndVisible("", textWidth),
1106
- h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1178
+ Box4,
1179
+ { key: `pad${i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1180
+ h5(Box4, { width: textWidth, flexShrink: 0 }, h5(Text5, {}, padEndVisible("", textWidth))),
1181
+ h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph))
1107
1182
  )
1108
1183
  );
1109
1184
  }
@@ -1121,7 +1196,10 @@ var init_scrollable_text = __esm({
1121
1196
  "src/screen/scrollable-text.js"() {
1122
1197
  init_components();
1123
1198
  init_scrollbar();
1199
+ init_follow_scroll();
1200
+ init_visible_text();
1124
1201
  init_scrollbar();
1202
+ init_visible_text();
1125
1203
  h5 = createElement2;
1126
1204
  SCROLL_KEYS = [
1127
1205
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
@@ -1279,6 +1357,8 @@ var init_screen = __esm({
1279
1357
  init_components();
1280
1358
  init_ui_elements();
1281
1359
  init_scrollable_text();
1360
+ init_visible_text();
1361
+ init_follow_scroll();
1282
1362
  init_scrollbar();
1283
1363
  init_key_bindings();
1284
1364
  init_utils();
@@ -10164,6 +10244,7 @@ export {
10164
10244
  buildDetailBreadcrumb,
10165
10245
  buildFooter,
10166
10246
  bumpPatchVersion,
10247
+ clampScroll,
10167
10248
  cloneRepo,
10168
10249
  coerceRuntimeValue,
10169
10250
  controlLaneTaskNames,
@@ -10201,6 +10282,7 @@ export {
10201
10282
  ipcFileLogsTableNameForSourceResource,
10202
10283
  isFreshOnline,
10203
10284
  isPidAlive,
10285
+ isScrolledToBottom,
10204
10286
  joiEdateType,
10205
10287
  joiStringArrayType,
10206
10288
  listServicesRegistry as listAliveRunnerHeartbeats,
@@ -10214,11 +10296,14 @@ export {
10214
10296
  memo,
10215
10297
  mergeAllowedTasksWithServiceTasks,
10216
10298
  mergeRuntimeParamSpecs,
10299
+ nextScrollAfterContentChange,
10300
+ nextScrollAfterUserMove,
10217
10301
  nextTimeMatch,
10218
10302
  normalizeAllowedTasks,
10219
10303
  npmEnv,
10220
10304
  npmInstallEnv,
10221
10305
  organizeFooterMessages,
10306
+ padEndVisible,
10222
10307
  parseGitHost,
10223
10308
  parsePm2Jlist,
10224
10309
  prepareGitHost,
@@ -10264,9 +10349,11 @@ export {
10264
10349
  showMultiColumnListWithPreviewScreen,
10265
10350
  showScreen,
10266
10351
  showWordGridScreen,
10352
+ sliceVisible,
10267
10353
  sshRun,
10268
10354
  startPm2,
10269
10355
  stopPm2,
10356
+ stripAnsi,
10270
10357
  syncEnv,
10271
10358
  taskHistoryInsertFromQueueRow,
10272
10359
  timeMatcher,
@@ -10283,6 +10370,7 @@ export {
10283
10370
  useMemo2 as useMemo,
10284
10371
  useRef3 as useRef,
10285
10372
  useState4 as useState,
10373
+ visibleWidth,
10286
10374
  waitForTaskResult,
10287
10375
  waitPm2,
10288
10376
  wrapTextLines,