@nmakarov/cli-toolkit 0.67.0 → 0.69.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/screen.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/screen/index.js
2
- import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
3
- import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
2
+ import React2, { useState as useState4, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useMemo as useMemo2, useCallback, memo, createElement as createElement3 } from "react";
3
+ import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
4
4
 
5
5
  // src/screen/screens.js
6
6
  import { useState as useState2, createElement as h3 } from "react";
@@ -459,6 +459,54 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
459
459
  );
460
460
  }
461
461
 
462
+ // src/screen/key-bindings.js
463
+ function bindingIdentity(b) {
464
+ return [
465
+ String(b?.key ?? ""),
466
+ b?.meta ? "m" : "",
467
+ b?.ctrl ? "c" : "",
468
+ b?.shift ? "s" : ""
469
+ ].join("|");
470
+ }
471
+ function bindingMatchesInput(binding, input, key) {
472
+ if (!binding?.key) return false;
473
+ let keyMatches = false;
474
+ if (key?.[binding.key]) {
475
+ keyMatches = true;
476
+ } else if (input === binding.key) {
477
+ keyMatches = true;
478
+ } else if (key?.name === binding.key) {
479
+ keyMatches = true;
480
+ }
481
+ if (!keyMatches) return false;
482
+ const modOk = (flag, pressed) => {
483
+ if (flag === true) return !!pressed;
484
+ if (flag === false) return !pressed;
485
+ return !pressed;
486
+ };
487
+ if (!modOk(binding.meta, key?.meta)) return false;
488
+ if (!modOk(binding.ctrl, key?.ctrl)) return false;
489
+ if (binding.shift !== void 0 && !!key?.shift !== !!binding.shift) return false;
490
+ return true;
491
+ }
492
+ var KEY_LABELS = {
493
+ escape: "esc",
494
+ leftArrow: "\u2190",
495
+ rightArrow: "\u2192",
496
+ upArrow: "\u2191",
497
+ downArrow: "\u2193",
498
+ return: "enter",
499
+ pageUp: "PgUp",
500
+ pageDown: "PgDn"
501
+ };
502
+ function formatBindingKey(binding) {
503
+ let label = KEY_LABELS[binding.key] || binding.key;
504
+ if (binding.shift) label = `\u21E7${label}`;
505
+ if (binding.ctrl) label = `^${label}`;
506
+ if (binding.meta) label = `\u2325${label}`;
507
+ return label;
508
+ }
509
+
462
510
  // src/screen/screens.js
463
511
  function groupKeyBindings(bindings) {
464
512
  const groups = {};
@@ -472,7 +520,7 @@ function groupKeyBindings(bindings) {
472
520
  order: binding.order || 999
473
521
  };
474
522
  }
475
- groups[caption].keys.push(binding.key);
523
+ groups[caption].keys.push(formatBindingKey(binding));
476
524
  });
477
525
  return Object.values(groups);
478
526
  }
@@ -512,12 +560,14 @@ function formatKeyBindings(bindings, mode = "long") {
512
560
  }
513
561
  function formatKeys(keys) {
514
562
  const keyMap = {
515
- "escape": "esc",
516
- "leftArrow": "\u2190",
517
- "rightArrow": "\u2192",
518
- "upArrow": "\u2191",
519
- "downArrow": "\u2193",
520
- "return": "enter"
563
+ escape: "esc",
564
+ leftArrow: "\u2190",
565
+ rightArrow: "\u2192",
566
+ upArrow: "\u2191",
567
+ downArrow: "\u2193",
568
+ return: "enter",
569
+ pageUp: "PgUp",
570
+ pageDown: "PgDn"
521
571
  };
522
572
  return keys.map((k) => keyMap[k] || k).join("/");
523
573
  }
@@ -557,7 +607,10 @@ async function showScreen(config) {
557
607
  setKeyBinding: (bindingOrBindings) => {
558
608
  const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
559
609
  bindingsToSet.forEach((binding) => {
560
- const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
610
+ const id = bindingIdentity(binding);
611
+ const existingIndex = keyBindings.findIndex(
612
+ (b) => bindingIdentity(b) === id
613
+ );
561
614
  if (existingIndex >= 0) {
562
615
  const existing = keyBindings[existingIndex];
563
616
  if (existing.protected) {
@@ -581,7 +634,10 @@ async function showScreen(config) {
581
634
  });
582
635
  },
583
636
  updateKeyBinding: (keyName, updates) => {
584
- const index = keyBindings.findIndex((b) => b.key === keyName);
637
+ const id = updates && (updates.meta != null || updates.ctrl != null || updates.shift != null) ? bindingIdentity({ key: keyName, ...updates }) : null;
638
+ const index = keyBindings.findIndex(
639
+ (b) => id ? bindingIdentity(b) === id : b.key === keyName
640
+ );
585
641
  if (index >= 0) {
586
642
  keyBindings[index] = {
587
643
  ...keyBindings[index],
@@ -589,11 +645,13 @@ async function showScreen(config) {
589
645
  };
590
646
  }
591
647
  },
592
- removeKeyBinding: (keyName) => {
593
- const index = keyBindings.findIndex((b) => b.key === keyName);
648
+ removeKeyBinding: (keyNameOrBinding) => {
649
+ const index = typeof keyNameOrBinding === "object" && keyNameOrBinding ? keyBindings.findIndex(
650
+ (b) => bindingIdentity(b) === bindingIdentity(keyNameOrBinding)
651
+ ) : keyBindings.findIndex((b) => b.key === keyNameOrBinding);
594
652
  if (index >= 0) {
595
653
  if (keyBindings[index].protected) {
596
- console.warn(`Cannot remove protected key: ${keyName}`);
654
+ console.warn(`Cannot remove protected key: ${keyNameOrBinding}`);
597
655
  return;
598
656
  }
599
657
  keyBindings.splice(index, 1);
@@ -634,24 +692,11 @@ async function showScreen(config) {
634
692
  }
635
693
  let matchedBinding = null;
636
694
  for (const binding of keyBindings) {
637
- let keyMatches = false;
638
- if (key[binding.key]) {
639
- keyMatches = true;
640
- } else if (input === binding.key) {
641
- keyMatches = true;
642
- } else if (key?.name === binding.key) {
643
- keyMatches = true;
644
- }
645
- if (keyMatches) {
646
- if (binding.enabled === false) {
647
- continue;
648
- }
649
- if (binding.condition && !binding.condition(context)) {
650
- continue;
651
- }
652
- matchedBinding = binding;
653
- break;
654
- }
695
+ if (binding.enabled === false) continue;
696
+ if (!bindingMatchesInput(binding, input, key)) continue;
697
+ if (binding.condition && !binding.condition(context)) continue;
698
+ matchedBinding = binding;
699
+ break;
655
700
  }
656
701
  if (matchedBinding && actions[matchedBinding.action]) {
657
702
  actions[matchedBinding.action]({
@@ -865,6 +910,155 @@ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit })
865
910
  );
866
911
  }
867
912
 
913
+ // src/screen/scrollable-text.js
914
+ import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
915
+ import { Box as Box4, Text as Text5 } from "ink";
916
+ var h5 = createElement2;
917
+ var BAR_THUMB = "#";
918
+ var BAR_TRACK = "|";
919
+ function wrapTextLines(text, cols) {
920
+ const w = Math.max(1, Math.floor(Number(cols) || 1));
921
+ const out = [];
922
+ for (const raw of String(text ?? "").split("\n")) {
923
+ if (raw.length === 0) {
924
+ out.push("");
925
+ continue;
926
+ }
927
+ let rest = raw;
928
+ while (rest.length > w) {
929
+ out.push(rest.slice(0, w));
930
+ rest = rest.slice(w);
931
+ }
932
+ if (rest.length > 0) out.push(rest);
933
+ }
934
+ return out;
935
+ }
936
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
937
+ const view = Math.max(1, Math.floor(viewportRows));
938
+ const total = Math.max(0, Math.floor(totalLines));
939
+ if (total <= view) return null;
940
+ const maxScroll = total - view;
941
+ const thumbSize = Math.max(1, Math.round(view / total * view));
942
+ const travel = Math.max(0, view - thumbSize);
943
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
944
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
945
+ const glyphs = [];
946
+ for (let i = 0; i < view; i++) {
947
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
948
+ }
949
+ return glyphs;
950
+ }
951
+ function padEndVisible(s, w) {
952
+ const t = String(s ?? "");
953
+ if (t.length >= w) return t.slice(0, w);
954
+ return t + " ".repeat(w - t.length);
955
+ }
956
+ var SCROLL_KEYS = [
957
+ { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
958
+ { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
959
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
960
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
961
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
962
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
963
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
964
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
965
+ ];
966
+ function ScrollableText({
967
+ ctx,
968
+ text = "",
969
+ lines: linesProp,
970
+ maxHeight,
971
+ wrap = true,
972
+ showScrollbar = true,
973
+ showStatus = true,
974
+ bindKeys = true,
975
+ header = null
976
+ }) {
977
+ const [scrollTop, setScrollTop] = useState3(0);
978
+ const [, bump] = useState3(0);
979
+ const termRows = process.stdout.rows || 24;
980
+ const viewportRows = Math.max(
981
+ 4,
982
+ maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
983
+ );
984
+ const contentCols = Math.max(20, getScreenWidth() - 4);
985
+ const barCols = showScrollbar ? 1 : 0;
986
+ const textWidth = Math.max(8, contentCols - barCols);
987
+ const allLines = useMemo(() => {
988
+ if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
989
+ return wrap ? wrapTextLines(text, textWidth) : String(text ?? "").split("\n");
990
+ }, [linesProp, text, wrap, textWidth]);
991
+ const needsBar = showScrollbar && allLines.length > viewportRows;
992
+ const maxScroll = Math.max(0, allLines.length - viewportRows);
993
+ const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
994
+ const visible = allLines.slice(clamped, clamped + viewportRows);
995
+ const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
996
+ useEffect2(() => {
997
+ setScrollTop((s) => Math.min(s, maxScroll));
998
+ }, [maxScroll]);
999
+ const maxScrollRef = useRef2(maxScroll);
1000
+ const pageSizeRef = useRef2(viewportRows);
1001
+ maxScrollRef.current = maxScroll;
1002
+ pageSizeRef.current = viewportRows;
1003
+ useEffect2(() => {
1004
+ if (!ctx || !bindKeys) return void 0;
1005
+ ctx.setKeyBinding(SCROLL_KEYS);
1006
+ ctx.setAction("scrollUp", () => {
1007
+ setScrollTop((s) => Math.max(0, s - 1));
1008
+ bump((n) => n + 1);
1009
+ ctx.update?.();
1010
+ });
1011
+ ctx.setAction("scrollDown", () => {
1012
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1013
+ bump((n) => n + 1);
1014
+ ctx.update?.();
1015
+ });
1016
+ ctx.setAction("pageUp", () => {
1017
+ setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1018
+ bump((n) => n + 1);
1019
+ ctx.update?.();
1020
+ });
1021
+ ctx.setAction("pageDown", () => {
1022
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1023
+ bump((n) => n + 1);
1024
+ ctx.update?.();
1025
+ });
1026
+ return void 0;
1027
+ }, [ctx, bindKeys]);
1028
+ 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" : "");
1029
+ const rowNodes = visible.map((line, i) => {
1030
+ const body = padEndVisible(line, textWidth);
1031
+ const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1032
+ const isThumb = glyph === BAR_THUMB;
1033
+ return h5(
1034
+ Text5,
1035
+ { key: `L${clamped + i}` },
1036
+ body,
1037
+ glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1038
+ );
1039
+ });
1040
+ if (bar && visible.length < viewportRows) {
1041
+ for (let i = visible.length; i < viewportRows; i++) {
1042
+ const glyph = bar[i] ?? BAR_TRACK;
1043
+ rowNodes.push(
1044
+ h5(
1045
+ Text5,
1046
+ { key: `pad${i}` },
1047
+ padEndVisible("", textWidth),
1048
+ h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1049
+ )
1050
+ );
1051
+ }
1052
+ }
1053
+ return h5(
1054
+ Box4,
1055
+ { flexDirection: "column" },
1056
+ header == null ? null : typeof header === "string" ? h5(Text5, { color: "gray" }, header) : header,
1057
+ showStatus ? h5(Text5, { color: "gray" }, status) : null,
1058
+ ...rowNodes
1059
+ );
1060
+ }
1061
+
868
1062
  // src/screen/utils.js
869
1063
  function buildBreadcrumb(parts) {
870
1064
  if (parts.length === 0) return "";
@@ -1000,7 +1194,7 @@ if (typeof window === "undefined") {
1000
1194
  });
1001
1195
  }
1002
1196
  export {
1003
- Box4 as Box,
1197
+ Box5 as Box,
1004
1198
  Divider,
1005
1199
  FooterPresets,
1006
1200
  GridCell,
@@ -1016,15 +1210,20 @@ export {
1016
1210
  ScreenFooter,
1017
1211
  ScreenRow,
1018
1212
  ScreenTitle,
1019
- Text5 as Text,
1213
+ ScrollableText,
1214
+ Text6 as Text,
1020
1215
  TextBlock,
1216
+ bindingIdentity,
1217
+ bindingMatchesInput,
1021
1218
  buildBreadcrumb,
1022
1219
  buildDetailBreadcrumb,
1023
1220
  buildFooter,
1024
- createElement2 as h,
1221
+ formatBindingKey,
1222
+ createElement3 as h,
1025
1223
  load,
1026
1224
  memo,
1027
1225
  organizeFooterMessages,
1226
+ scrollbarGlyphs,
1028
1227
  showListScreen,
1029
1228
  showMenuScreen,
1030
1229
  showMultiColumnListScreen,
@@ -1032,11 +1231,12 @@ export {
1032
1231
  showScreen,
1033
1232
  showWordGridScreen,
1034
1233
  useCallback,
1035
- useEffect2 as useEffect,
1234
+ useEffect3 as useEffect,
1036
1235
  useInput2 as useInput,
1037
1236
  useLayoutEffect,
1038
- useMemo,
1039
- useRef2 as useRef,
1040
- useState3 as useState
1237
+ useMemo2 as useMemo,
1238
+ useRef3 as useRef,
1239
+ useState4 as useState,
1240
+ wrapTextLines
1041
1241
  };
1042
1242
  //# sourceMappingURL=screen.js.map