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