@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.
@@ -1002,9 +1002,68 @@ var init_ui_elements = __esm({
1002
1002
  }
1003
1003
  });
1004
1004
 
1005
- // src/screen/scrollable-text.js
1006
- import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
1007
- import { Box as Box4, Text as Text5 } from "ink";
1005
+ // src/screen/follow-scroll.js
1006
+ function clampScroll(scrollTop, maxScroll) {
1007
+ const max = Math.max(0, Number(maxScroll) || 0);
1008
+ const top = Number(scrollTop) || 0;
1009
+ return Math.min(Math.max(0, top), max);
1010
+ }
1011
+ function isScrolledToBottom(scrollTop, maxScroll) {
1012
+ return clampScroll(scrollTop, maxScroll) >= Math.max(0, Number(maxScroll) || 0);
1013
+ }
1014
+ function nextScrollAfterUserMove(scrollTop, maxScroll, delta) {
1015
+ const next = clampScroll((Number(scrollTop) || 0) + (Number(delta) || 0), maxScroll);
1016
+ return { scrollTop: next, following: isScrolledToBottom(next, maxScroll) };
1017
+ }
1018
+ function nextScrollAfterContentChange({ following, scrollTop, maxScroll }) {
1019
+ const max = Math.max(0, Number(maxScroll) || 0);
1020
+ if (following) return { scrollTop: max, following: true };
1021
+ const next = clampScroll(scrollTop, max);
1022
+ return { scrollTop: next, following: isScrolledToBottom(next, max) };
1023
+ }
1024
+ var init_follow_scroll = __esm({
1025
+ "src/screen/follow-scroll.js"() {
1026
+ }
1027
+ });
1028
+
1029
+ // src/screen/visible-text.js
1030
+ function stripAnsi(text) {
1031
+ return String(text ?? "").replace(ANSI_RE, "");
1032
+ }
1033
+ function visibleWidth(text) {
1034
+ return [...stripAnsi(text)].length;
1035
+ }
1036
+ function sliceVisible(text, cols) {
1037
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1038
+ const s = String(text ?? "");
1039
+ if (w === 0) return "";
1040
+ let vis = 0;
1041
+ let out = "";
1042
+ let i = 0;
1043
+ while (i < s.length && vis < w) {
1044
+ ANSI_RE.lastIndex = i;
1045
+ const m = ANSI_RE.exec(s);
1046
+ if (m && m.index === i) {
1047
+ out += m[0];
1048
+ i += m[0].length;
1049
+ continue;
1050
+ }
1051
+ const cp = s.codePointAt(i);
1052
+ const ch = String.fromCodePoint(cp);
1053
+ out += ch;
1054
+ i += ch.length;
1055
+ vis += 1;
1056
+ }
1057
+ return out;
1058
+ }
1059
+ function padEndVisible(text, cols) {
1060
+ const w = Math.max(0, Math.floor(Number(cols) || 0));
1061
+ const s = String(text ?? "");
1062
+ const vis = visibleWidth(s);
1063
+ if (vis === w) return s;
1064
+ if (vis > w) return sliceVisible(s, w);
1065
+ return s + " ".repeat(w - vis);
1066
+ }
1008
1067
  function wrapTextLines(text, cols) {
1009
1068
  const w = Math.max(1, Math.floor(Number(cols) || 1));
1010
1069
  const out = [];
@@ -1013,20 +1072,30 @@ function wrapTextLines(text, cols) {
1013
1072
  out.push("");
1014
1073
  continue;
1015
1074
  }
1075
+ if (visibleWidth(raw) <= w) {
1076
+ out.push(raw);
1077
+ continue;
1078
+ }
1016
1079
  let rest = raw;
1017
- while (rest.length > w) {
1018
- out.push(rest.slice(0, w));
1019
- rest = rest.slice(w);
1080
+ while (visibleWidth(rest) > w) {
1081
+ const head = sliceVisible(rest, w);
1082
+ out.push(head);
1083
+ rest = rest.slice(head.length);
1020
1084
  }
1021
1085
  if (rest.length > 0) out.push(rest);
1022
1086
  }
1023
1087
  return out;
1024
1088
  }
1025
- function padEndVisible(s, w) {
1026
- const t = String(s ?? "");
1027
- if (t.length >= w) return t.slice(0, w);
1028
- return t + " ".repeat(w - t.length);
1029
- }
1089
+ var ANSI_RE;
1090
+ var init_visible_text = __esm({
1091
+ "src/screen/visible-text.js"() {
1092
+ ANSI_RE = /\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1093
+ }
1094
+ });
1095
+
1096
+ // src/screen/scrollable-text.js
1097
+ import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
1098
+ import { Box as Box4, Text as Text5 } from "ink";
1030
1099
  function ScrollableText({
1031
1100
  ctx,
1032
1101
  text = "",
@@ -1036,9 +1105,11 @@ function ScrollableText({
1036
1105
  showScrollbar = true,
1037
1106
  showStatus = true,
1038
1107
  bindKeys = true,
1039
- header = null
1108
+ header = null,
1109
+ followBottom = false
1040
1110
  }) {
1041
1111
  const [scrollTop, setScrollTop] = useState3(0);
1112
+ const [following, setFollowing] = useState3(() => !!followBottom);
1042
1113
  const [, bump] = useState3(0);
1043
1114
  const termRows = process.stdout.rows || 24;
1044
1115
  const viewportRows = Math.max(
@@ -1057,48 +1128,52 @@ function ScrollableText({
1057
1128
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1058
1129
  const visible = allLines.slice(clamped, clamped + viewportRows);
1059
1130
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1060
- useEffect2(() => {
1061
- setScrollTop((s) => Math.min(s, maxScroll));
1062
- }, [maxScroll]);
1063
1131
  const maxScrollRef = useRef2(maxScroll);
1064
1132
  const pageSizeRef = useRef2(viewportRows);
1133
+ const scrollTopRef = useRef2(scrollTop);
1134
+ const followingRef = useRef2(following);
1065
1135
  maxScrollRef.current = maxScroll;
1066
1136
  pageSizeRef.current = viewportRows;
1137
+ scrollTopRef.current = scrollTop;
1138
+ followingRef.current = following;
1139
+ useEffect2(() => {
1140
+ const next = nextScrollAfterContentChange({
1141
+ following: followBottom && followingRef.current,
1142
+ scrollTop: scrollTopRef.current,
1143
+ maxScroll
1144
+ });
1145
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1146
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1147
+ }, [maxScroll, followBottom]);
1148
+ const applyUserScroll = (delta) => {
1149
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1150
+ setScrollTop(next.scrollTop);
1151
+ if (followBottom) setFollowing(next.following);
1152
+ bump((n) => n + 1);
1153
+ ctx?.update?.();
1154
+ };
1067
1155
  useEffect2(() => {
1068
1156
  if (!ctx || !bindKeys) return void 0;
1069
1157
  ctx.setKeyBinding(SCROLL_KEYS);
1070
- ctx.setAction("scrollUp", () => {
1071
- setScrollTop((s) => Math.max(0, s - 1));
1072
- bump((n) => n + 1);
1073
- ctx.update?.();
1074
- });
1075
- ctx.setAction("scrollDown", () => {
1076
- setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1077
- bump((n) => n + 1);
1078
- ctx.update?.();
1079
- });
1080
- ctx.setAction("pageUp", () => {
1081
- setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1082
- bump((n) => n + 1);
1083
- ctx.update?.();
1084
- });
1085
- ctx.setAction("pageDown", () => {
1086
- setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1087
- bump((n) => n + 1);
1088
- ctx.update?.();
1089
- });
1158
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1159
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1160
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1161
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1090
1162
  return void 0;
1091
- }, [ctx, bindKeys]);
1092
- 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" : "");
1163
+ }, [ctx, bindKeys, followBottom]);
1164
+ 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" : "");
1093
1165
  const rowNodes = visible.map((line, i) => {
1094
- const body = padEndVisible(line, textWidth);
1095
1166
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1096
1167
  const isThumb = glyph === BAR_THUMB;
1097
1168
  return h5(
1098
- Text5,
1099
- { key: `L${clamped + i}` },
1100
- body,
1101
- glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1169
+ Box4,
1170
+ { key: `L${clamped + i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1171
+ h5(
1172
+ Box4,
1173
+ { width: textWidth, flexShrink: 0 },
1174
+ h5(Text5, { wrap: "truncate" }, padEndVisible(line, textWidth))
1175
+ ),
1176
+ showScrollbar ? h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph)) : null
1102
1177
  );
1103
1178
  });
1104
1179
  if (bar && visible.length < viewportRows) {
@@ -1106,10 +1181,10 @@ function ScrollableText({
1106
1181
  const glyph = bar[i] ?? BAR_TRACK;
1107
1182
  rowNodes.push(
1108
1183
  h5(
1109
- Text5,
1110
- { key: `pad${i}` },
1111
- padEndVisible("", textWidth),
1112
- h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1184
+ Box4,
1185
+ { key: `pad${i}`, flexDirection: "row", width: contentCols, flexShrink: 0 },
1186
+ h5(Box4, { width: textWidth, flexShrink: 0 }, h5(Text5, {}, padEndVisible("", textWidth))),
1187
+ h5(Box4, { width: 1, flexShrink: 0 }, h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph))
1113
1188
  )
1114
1189
  );
1115
1190
  }
@@ -1127,7 +1202,10 @@ var init_scrollable_text = __esm({
1127
1202
  "src/screen/scrollable-text.js"() {
1128
1203
  init_components();
1129
1204
  init_scrollbar();
1205
+ init_follow_scroll();
1206
+ init_visible_text();
1130
1207
  init_scrollbar();
1208
+ init_visible_text();
1131
1209
  h5 = createElement2;
1132
1210
  SCROLL_KEYS = [
1133
1211
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
@@ -1295,11 +1373,16 @@ __export(screen_exports, {
1295
1373
  buildBreadcrumb: () => buildBreadcrumb,
1296
1374
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1297
1375
  buildFooter: () => buildFooter,
1376
+ clampScroll: () => clampScroll,
1298
1377
  formatBindingKey: () => formatBindingKey,
1299
1378
  h: () => createElement3,
1379
+ isScrolledToBottom: () => isScrolledToBottom,
1300
1380
  load: () => load,
1301
1381
  memo: () => memo,
1382
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
1383
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
1302
1384
  organizeFooterMessages: () => organizeFooterMessages,
1385
+ padEndVisible: () => padEndVisible,
1303
1386
  scrollbarGlyphs: () => scrollbarGlyphs,
1304
1387
  showListScreen: () => showListScreen,
1305
1388
  showMenuScreen: () => showMenuScreen,
@@ -1307,6 +1390,8 @@ __export(screen_exports, {
1307
1390
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1308
1391
  showScreen: () => showScreen,
1309
1392
  showWordGridScreen: () => showWordGridScreen,
1393
+ sliceVisible: () => sliceVisible,
1394
+ stripAnsi: () => stripAnsi,
1310
1395
  useCallback: () => useCallback,
1311
1396
  useEffect: () => useEffect3,
1312
1397
  useInput: () => useInput2,
@@ -1314,6 +1399,7 @@ __export(screen_exports, {
1314
1399
  useMemo: () => useMemo2,
1315
1400
  useRef: () => useRef3,
1316
1401
  useState: () => useState4,
1402
+ visibleWidth: () => visibleWidth,
1317
1403
  wrapTextLines: () => wrapTextLines
1318
1404
  });
1319
1405
  import React2, { useState as useState4, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useMemo as useMemo2, useCallback, memo, createElement as createElement3 } from "react";
@@ -1335,6 +1421,8 @@ var init_screen = __esm({
1335
1421
  init_components();
1336
1422
  init_ui_elements();
1337
1423
  init_scrollable_text();
1424
+ init_visible_text();
1425
+ init_follow_scroll();
1338
1426
  init_scrollbar();
1339
1427
  init_key_bindings();
1340
1428
  init_utils();