@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.
@@ -1002,6 +1002,30 @@ var init_ui_elements = __esm({
1002
1002
  }
1003
1003
  });
1004
1004
 
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
+
1005
1029
  // src/screen/scrollable-text.js
1006
1030
  import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
1007
1031
  import { Box as Box4, Text as Text5 } from "ink";
@@ -1036,9 +1060,11 @@ function ScrollableText({
1036
1060
  showScrollbar = true,
1037
1061
  showStatus = true,
1038
1062
  bindKeys = true,
1039
- header = null
1063
+ header = null,
1064
+ followBottom = false
1040
1065
  }) {
1041
1066
  const [scrollTop, setScrollTop] = useState3(0);
1067
+ const [following, setFollowing] = useState3(() => !!followBottom);
1042
1068
  const [, bump] = useState3(0);
1043
1069
  const termRows = process.stdout.rows || 24;
1044
1070
  const viewportRows = Math.max(
@@ -1057,39 +1083,40 @@ function ScrollableText({
1057
1083
  const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1058
1084
  const visible = allLines.slice(clamped, clamped + viewportRows);
1059
1085
  const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1060
- useEffect2(() => {
1061
- setScrollTop((s) => Math.min(s, maxScroll));
1062
- }, [maxScroll]);
1063
1086
  const maxScrollRef = useRef2(maxScroll);
1064
1087
  const pageSizeRef = useRef2(viewportRows);
1088
+ const scrollTopRef = useRef2(scrollTop);
1089
+ const followingRef = useRef2(following);
1065
1090
  maxScrollRef.current = maxScroll;
1066
1091
  pageSizeRef.current = viewportRows;
1092
+ scrollTopRef.current = scrollTop;
1093
+ followingRef.current = following;
1094
+ useEffect2(() => {
1095
+ const next = nextScrollAfterContentChange({
1096
+ following: followBottom && followingRef.current,
1097
+ scrollTop: scrollTopRef.current,
1098
+ maxScroll
1099
+ });
1100
+ if (next.scrollTop !== scrollTopRef.current) setScrollTop(next.scrollTop);
1101
+ if (followBottom && next.following !== followingRef.current) setFollowing(next.following);
1102
+ }, [maxScroll, followBottom]);
1103
+ const applyUserScroll = (delta) => {
1104
+ const next = nextScrollAfterUserMove(scrollTopRef.current, maxScrollRef.current, delta);
1105
+ setScrollTop(next.scrollTop);
1106
+ if (followBottom) setFollowing(next.following);
1107
+ bump((n) => n + 1);
1108
+ ctx?.update?.();
1109
+ };
1067
1110
  useEffect2(() => {
1068
1111
  if (!ctx || !bindKeys) return void 0;
1069
1112
  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
- });
1113
+ ctx.setAction("scrollUp", () => applyUserScroll(-1));
1114
+ ctx.setAction("scrollDown", () => applyUserScroll(1));
1115
+ ctx.setAction("pageUp", () => applyUserScroll(-pageSizeRef.current));
1116
+ ctx.setAction("pageDown", () => applyUserScroll(pageSizeRef.current));
1090
1117
  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" : "");
1118
+ }, [ctx, bindKeys, followBottom]);
1119
+ 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
1120
  const rowNodes = visible.map((line, i) => {
1094
1121
  const body = padEndVisible(line, textWidth);
1095
1122
  const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
@@ -1127,6 +1154,7 @@ var init_scrollable_text = __esm({
1127
1154
  "src/screen/scrollable-text.js"() {
1128
1155
  init_components();
1129
1156
  init_scrollbar();
1157
+ init_follow_scroll();
1130
1158
  init_scrollbar();
1131
1159
  h5 = createElement2;
1132
1160
  SCROLL_KEYS = [
@@ -1295,10 +1323,14 @@ __export(screen_exports, {
1295
1323
  buildBreadcrumb: () => buildBreadcrumb,
1296
1324
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1297
1325
  buildFooter: () => buildFooter,
1326
+ clampScroll: () => clampScroll,
1298
1327
  formatBindingKey: () => formatBindingKey,
1299
1328
  h: () => createElement3,
1329
+ isScrolledToBottom: () => isScrolledToBottom,
1300
1330
  load: () => load,
1301
1331
  memo: () => memo,
1332
+ nextScrollAfterContentChange: () => nextScrollAfterContentChange,
1333
+ nextScrollAfterUserMove: () => nextScrollAfterUserMove,
1302
1334
  organizeFooterMessages: () => organizeFooterMessages,
1303
1335
  scrollbarGlyphs: () => scrollbarGlyphs,
1304
1336
  showListScreen: () => showListScreen,
@@ -1335,6 +1367,7 @@ var init_screen = __esm({
1335
1367
  init_components();
1336
1368
  init_ui_elements();
1337
1369
  init_scrollable_text();
1370
+ init_follow_scroll();
1338
1371
  init_scrollbar();
1339
1372
  init_key_bindings();
1340
1373
  init_utils();