@nmakarov/cli-toolkit 0.80.0 → 0.81.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.
@@ -1018,6 +1018,30 @@ var init_ui_elements = __esm({
1018
1018
  }
1019
1019
  });
1020
1020
 
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
+
1021
1045
  // src/screen/scrollable-text.js
1022
1046
  function wrapTextLines(text, cols) {
1023
1047
  const w = Math.max(1, Math.floor(Number(cols) || 1));
@@ -1050,9 +1074,11 @@ function ScrollableText({
1050
1074
  showScrollbar = true,
1051
1075
  showStatus = true,
1052
1076
  bindKeys = true,
1053
- header = null
1077
+ header = null,
1078
+ followBottom = false
1054
1079
  }) {
1055
1080
  const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
1081
+ const [following, setFollowing] = (0, import_react5.useState)(() => !!followBottom);
1056
1082
  const [, bump] = (0, import_react5.useState)(0);
1057
1083
  const termRows = process.stdout.rows || 24;
1058
1084
  const viewportRows = Math.max(
@@ -1071,39 +1097,40 @@ function ScrollableText({
1071
1097
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1072
1098
  const visible = allLines.slice(clamped, clamped + viewportRows);
1073
1099
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1074
- (0, import_react5.useEffect)(() => {
1075
- setScrollTop((s) => Math.min(s, maxScroll));
1076
- }, [maxScroll]);
1077
1100
  const maxScrollRef = (0, import_react5.useRef)(maxScroll);
1078
1101
  const pageSizeRef = (0, import_react5.useRef)(viewportRows);
1102
+ const scrollTopRef = (0, import_react5.useRef)(scrollTop);
1103
+ const followingRef = (0, import_react5.useRef)(following);
1079
1104
  maxScrollRef.current = maxScroll;
1080
1105
  pageSizeRef.current = viewportRows;
1106
+ scrollTopRef.current = scrollTop;
1107
+ followingRef.current = following;
1108
+ (0, import_react5.useEffect)(() => {
1109
+ const next = nextScrollAfterContentChange({
1110
+ following: followBottom && followingRef.current,
1111
+ scrollTop: scrollTopRef.current,
1112
+ maxScroll
1113
+ });
1114
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1115
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1116
+ }, [maxScroll, followBottom]);
1117
+ const applyUserScroll = (delta) => {
1118
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1119
+ setScrollTop(next.scrollTop);
1120
+ if (followBottom) setFollowing(next.following);
1121
+ bump((n) => n + 1);
1122
+ ctx?.update?.();
1123
+ };
1081
1124
  (0, import_react5.useEffect)(() => {
1082
1125
  if (!ctx || !bindKeys) return void 0;
1083
1126
  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
- });
1127
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1128
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1129
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1130
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1104
1131
  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" : "");
1132
+ }, [ctx, bindKeys, followBottom]);
1133
+ 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
1134
  const rowNodes = visible.map((line, i) => {
1108
1135
  const body = padEndVisible(line, textWidth);
1109
1136
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
@@ -1143,6 +1170,7 @@ var init_scrollable_text = __esm({
1143
1170
  import_ink5 = require("ink");
1144
1171
  init_components();
1145
1172
  init_scrollbar();
1173
+ init_follow_scroll();
1146
1174
  init_scrollbar();
1147
1175
  h5 = import_react5.createElement;
1148
1176
  SCROLL_KEYS = [
@@ -1311,10 +1339,14 @@ __export(screen_exports, {
1311
1339
  buildBreadcrumb: () => buildBreadcrumb,
1312
1340
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1313
1341
  buildFooter: () => buildFooter,
1342
+ clampScroll: () => clampScroll,
1314
1343
  formatBindingKey: () => formatBindingKey,
1315
1344
  h: () => import_react6.createElement,
1345
+ isScrolledToBottom: () => isScrolledToBottom,
1316
1346
  load: () => load,
1317
1347
  memo: () => import_react6.memo,
1348
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
1349
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
1318
1350
  organizeFooterMessages: () => organizeFooterMessages,
1319
1351
  scrollbarGlyphs: () => scrollbarGlyphs,
1320
1352
  showListScreen: () => showListScreen,
@@ -1351,6 +1383,7 @@ var init_screen = __esm({
1351
1383
  init_components();
1352
1384
  init_ui_elements();
1353
1385
  init_scrollable_text();
1386
+ init_follow_scroll();
1354
1387
  init_scrollbar();
1355
1388
  init_key_bindings();
1356
1389
  init_utils();