@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.cjs CHANGED
@@ -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 cp3 = s.codePointAt(i);
1068
+ const ch = String.fromCodePoint(cp3);
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 },
@@ -1298,6 +1376,8 @@ var init_screen = __esm({
1298
1376
  init_components();
1299
1377
  init_ui_elements();
1300
1378
  init_scrollable_text();
1379
+ init_visible_text();
1380
+ init_follow_scroll();
1301
1381
  init_scrollbar();
1302
1382
  init_key_bindings();
1303
1383
  init_utils();
@@ -1377,6 +1457,7 @@ __export(src_exports, {
1377
1457
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1378
1458
  buildFooter: () => buildFooter,
1379
1459
  bumpPatchVersion: () => bumpPatchVersion,
1460
+ clampScroll: () => clampScroll,
1380
1461
  cloneRepo: () => cloneRepo,
1381
1462
  coerceRuntimeValue: () => coerceRuntimeValue,
1382
1463
  controlLaneTaskNames: () => controlLaneTaskNames,
@@ -1414,6 +1495,7 @@ __export(src_exports, {
1414
1495
  ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
1415
1496
  isFreshOnline: () => isFreshOnline,
1416
1497
  isPidAlive: () => isPidAlive,
1498
+ isScrolledToBottom: () => isScrolledToBottom,
1417
1499
  joiEdateType: () => joiEdateType,
1418
1500
  joiStringArrayType: () => joiStringArrayType,
1419
1501
  listAliveRunnerHeartbeats: () => listServicesRegistry,
@@ -1427,11 +1509,14 @@ __export(src_exports, {
1427
1509
  memo: () => import_react6.memo,
1428
1510
  mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
1429
1511
  mergeRuntimeParamSpecs: () => mergeRuntimeParamSpecs,
1512
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
1513
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
1430
1514
  nextTimeMatch: () => nextTimeMatch,
1431
1515
  normalizeAllowedTasks: () => normalizeAllowedTasks,
1432
1516
  npmEnv: () => npmEnv,
1433
1517
  npmInstallEnv: () => npmInstallEnv,
1434
1518
  organizeFooterMessages: () => organizeFooterMessages,
1519
+ padEndVisible: () => padEndVisible,
1435
1520
  parseGitHost: () => parseGitHost,
1436
1521
  parsePm2Jlist: () => parsePm2Jlist,
1437
1522
  prepareGitHost: () => prepareGitHost,
@@ -1477,9 +1562,11 @@ __export(src_exports, {
1477
1562
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1478
1563
  showScreen: () => showScreen,
1479
1564
  showWordGridScreen: () => showWordGridScreen,
1565
+ sliceVisible: () => sliceVisible,
1480
1566
  sshRun: () => sshRun,
1481
1567
  startPm2: () => startPm2,
1482
1568
  stopPm2: () => stopPm2,
1569
+ stripAnsi: () => stripAnsi,
1483
1570
  syncEnv: () => syncEnv,
1484
1571
  taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
1485
1572
  timeMatcher: () => timeMatcher,
@@ -1496,6 +1583,7 @@ __export(src_exports, {
1496
1583
  useMemo: () => import_react6.useMemo,
1497
1584
  useRef: () => import_react6.useRef,
1498
1585
  useState: () => import_react6.useState,
1586
+ visibleWidth: () => visibleWidth,
1499
1587
  waitForTaskResult: () => waitForTaskResult,
1500
1588
  waitPm2: () => waitPm2,
1501
1589
  wrapTextLines: () => wrapTextLines,
@@ -10351,6 +10439,7 @@ var TasksManager = class _TasksManager {
10351
10439
  buildDetailBreadcrumb,
10352
10440
  buildFooter,
10353
10441
  bumpPatchVersion,
10442
+ clampScroll,
10354
10443
  cloneRepo,
10355
10444
  coerceRuntimeValue,
10356
10445
  controlLaneTaskNames,
@@ -10388,6 +10477,7 @@ var TasksManager = class _TasksManager {
10388
10477
  ipcFileLogsTableNameForSourceResource,
10389
10478
  isFreshOnline,
10390
10479
  isPidAlive,
10480
+ isScrolledToBottom,
10391
10481
  joiEdateType,
10392
10482
  joiStringArrayType,
10393
10483
  listAliveRunnerHeartbeats,
@@ -10401,11 +10491,14 @@ var TasksManager = class _TasksManager {
10401
10491
  memo,
10402
10492
  mergeAllowedTasksWithServiceTasks,
10403
10493
  mergeRuntimeParamSpecs,
10494
+ nextScrollAfterContentChange,
10495
+ nextScrollAfterUserMove,
10404
10496
  nextTimeMatch,
10405
10497
  normalizeAllowedTasks,
10406
10498
  npmEnv,
10407
10499
  npmInstallEnv,
10408
10500
  organizeFooterMessages,
10501
+ padEndVisible,
10409
10502
  parseGitHost,
10410
10503
  parsePm2Jlist,
10411
10504
  prepareGitHost,
@@ -10451,9 +10544,11 @@ var TasksManager = class _TasksManager {
10451
10544
  showMultiColumnListWithPreviewScreen,
10452
10545
  showScreen,
10453
10546
  showWordGridScreen,
10547
+ sliceVisible,
10454
10548
  sshRun,
10455
10549
  startPm2,
10456
10550
  stopPm2,
10551
+ stripAnsi,
10457
10552
  syncEnv,
10458
10553
  taskHistoryInsertFromQueueRow,
10459
10554
  timeMatcher,
@@ -10470,6 +10565,7 @@ var TasksManager = class _TasksManager {
10470
10565
  useMemo,
10471
10566
  useRef,
10472
10567
  useState,
10568
+ visibleWidth,
10473
10569
  waitForTaskResult,
10474
10570
  waitPm2,
10475
10571
  wrapTextLines,