@nmakarov/cli-toolkit 0.67.0 → 0.68.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,152 @@ 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
+ function wrapTextLines(text, cols) {
918
+ const w = Math.max(1, Math.floor(Number(cols) || 1));
919
+ const out = [];
920
+ for (const raw of String(text ?? "").split("\n")) {
921
+ if (raw.length === 0) {
922
+ out.push("");
923
+ continue;
924
+ }
925
+ let rest = raw;
926
+ while (rest.length > w) {
927
+ out.push(rest.slice(0, w));
928
+ rest = rest.slice(w);
929
+ }
930
+ if (rest.length > 0) out.push(rest);
931
+ }
932
+ return out;
933
+ }
934
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
935
+ const view = Math.max(1, Math.floor(viewportRows));
936
+ const total = Math.max(0, Math.floor(totalLines));
937
+ if (total <= view) return null;
938
+ const maxScroll = total - view;
939
+ const thumbSize = Math.max(1, Math.round(view / total * view));
940
+ const travel = Math.max(0, view - thumbSize);
941
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
942
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
943
+ const glyphs = [];
944
+ for (let i = 0; i < view; i++) {
945
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
946
+ }
947
+ return glyphs;
948
+ }
949
+ function padEndVisible(s, w) {
950
+ const t = String(s ?? "");
951
+ if (t.length >= w) return t.slice(0, w);
952
+ return t + " ".repeat(w - t.length);
953
+ }
954
+ var SCROLL_KEYS = [
955
+ { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
956
+ { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
957
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
958
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
959
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
960
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
961
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
962
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
963
+ ];
964
+ function ScrollableText({
965
+ ctx,
966
+ text = "",
967
+ lines: linesProp,
968
+ maxHeight,
969
+ wrap = true,
970
+ showScrollbar = true,
971
+ showStatus = true,
972
+ bindKeys = true,
973
+ header = null
974
+ }) {
975
+ const [scrollTop, setScrollTop] = useState3(0);
976
+ const [, bump] = useState3(0);
977
+ const termCols = process.stdout.columns || 80;
978
+ const termRows = process.stdout.rows || 24;
979
+ const viewportRows = Math.max(
980
+ 4,
981
+ maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
982
+ );
983
+ const provisionalBar = showScrollbar ? 1 : 0;
984
+ const wrapCols = Math.max(8, termCols - provisionalBar);
985
+ const allLines = useMemo(() => {
986
+ if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
987
+ return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
988
+ }, [linesProp, text, wrap, wrapCols]);
989
+ const needsBar = showScrollbar && allLines.length > viewportRows;
990
+ const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
991
+ const maxScroll = Math.max(0, allLines.length - viewportRows);
992
+ const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
993
+ const visible = allLines.slice(clamped, clamped + viewportRows);
994
+ const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
995
+ useEffect2(() => {
996
+ setScrollTop((s) => Math.min(s, maxScroll));
997
+ }, [maxScroll]);
998
+ const maxScrollRef = useRef2(maxScroll);
999
+ const pageSizeRef = useRef2(viewportRows);
1000
+ maxScrollRef.current = maxScroll;
1001
+ pageSizeRef.current = viewportRows;
1002
+ useEffect2(() => {
1003
+ if (!ctx || !bindKeys) return void 0;
1004
+ ctx.setKeyBinding(SCROLL_KEYS);
1005
+ ctx.setAction("scrollUp", () => {
1006
+ setScrollTop((s) => Math.max(0, s - 1));
1007
+ bump((n) => n + 1);
1008
+ ctx.update?.();
1009
+ });
1010
+ ctx.setAction("scrollDown", () => {
1011
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1012
+ bump((n) => n + 1);
1013
+ ctx.update?.();
1014
+ });
1015
+ ctx.setAction("pageUp", () => {
1016
+ setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1017
+ bump((n) => n + 1);
1018
+ ctx.update?.();
1019
+ });
1020
+ ctx.setAction("pageDown", () => {
1021
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1022
+ bump((n) => n + 1);
1023
+ ctx.update?.();
1024
+ });
1025
+ return void 0;
1026
+ }, [ctx, bindKeys]);
1027
+ 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" : "");
1028
+ const rowNodes = visible.map((line, i) => {
1029
+ const body = padEndVisible(line, textWidth);
1030
+ const glyph = bar ? bar[i] ?? "\u2502" : "";
1031
+ return h5(
1032
+ Box4,
1033
+ { key: `L${clamped + i}`, flexDirection: "row" },
1034
+ h5(Text5, {}, body),
1035
+ glyph ? h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1036
+ );
1037
+ });
1038
+ if (bar && visible.length < viewportRows) {
1039
+ for (let i = visible.length; i < viewportRows; i++) {
1040
+ rowNodes.push(
1041
+ h5(
1042
+ Box4,
1043
+ { key: `pad${i}`, flexDirection: "row" },
1044
+ h5(Text5, {}, padEndVisible("", textWidth)),
1045
+ h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1046
+ )
1047
+ );
1048
+ }
1049
+ }
1050
+ return h5(
1051
+ Box4,
1052
+ { flexDirection: "column" },
1053
+ header == null ? null : typeof header === "string" ? h5(Text5, { color: "gray" }, header) : header,
1054
+ showStatus ? h5(Text5, { color: "gray" }, status) : null,
1055
+ ...rowNodes
1056
+ );
1057
+ }
1058
+
868
1059
  // src/screen/utils.js
869
1060
  function buildBreadcrumb(parts) {
870
1061
  if (parts.length === 0) return "";
@@ -1000,7 +1191,7 @@ if (typeof window === "undefined") {
1000
1191
  });
1001
1192
  }
1002
1193
  export {
1003
- Box4 as Box,
1194
+ Box5 as Box,
1004
1195
  Divider,
1005
1196
  FooterPresets,
1006
1197
  GridCell,
@@ -1016,15 +1207,20 @@ export {
1016
1207
  ScreenFooter,
1017
1208
  ScreenRow,
1018
1209
  ScreenTitle,
1019
- Text5 as Text,
1210
+ ScrollableText,
1211
+ Text6 as Text,
1020
1212
  TextBlock,
1213
+ bindingIdentity,
1214
+ bindingMatchesInput,
1021
1215
  buildBreadcrumb,
1022
1216
  buildDetailBreadcrumb,
1023
1217
  buildFooter,
1024
- createElement2 as h,
1218
+ formatBindingKey,
1219
+ createElement3 as h,
1025
1220
  load,
1026
1221
  memo,
1027
1222
  organizeFooterMessages,
1223
+ scrollbarGlyphs,
1028
1224
  showListScreen,
1029
1225
  showMenuScreen,
1030
1226
  showMultiColumnListScreen,
@@ -1032,11 +1228,12 @@ export {
1032
1228
  showScreen,
1033
1229
  showWordGridScreen,
1034
1230
  useCallback,
1035
- useEffect2 as useEffect,
1231
+ useEffect3 as useEffect,
1036
1232
  useInput2 as useInput,
1037
1233
  useLayoutEffect,
1038
- useMemo,
1039
- useRef2 as useRef,
1040
- useState3 as useState
1234
+ useMemo2 as useMemo,
1235
+ useRef3 as useRef,
1236
+ useState4 as useState,
1237
+ wrapTextLines
1041
1238
  };
1042
1239
  //# sourceMappingURL=screen.js.map