@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/init.js CHANGED
@@ -477,6 +477,59 @@ var init_list_components = __esm({
477
477
  }
478
478
  });
479
479
 
480
+ // src/screen/key-bindings.js
481
+ function bindingIdentity(b) {
482
+ return [
483
+ String(b?.key ?? ""),
484
+ b?.meta ? "m" : "",
485
+ b?.ctrl ? "c" : "",
486
+ b?.shift ? "s" : ""
487
+ ].join("|");
488
+ }
489
+ function bindingMatchesInput(binding, input, key) {
490
+ if (!binding?.key) return false;
491
+ let keyMatches = false;
492
+ if (key?.[binding.key]) {
493
+ keyMatches = true;
494
+ } else if (input === binding.key) {
495
+ keyMatches = true;
496
+ } else if (key?.name === binding.key) {
497
+ keyMatches = true;
498
+ }
499
+ if (!keyMatches) return false;
500
+ const modOk = (flag, pressed) => {
501
+ if (flag === true) return !!pressed;
502
+ if (flag === false) return !pressed;
503
+ return !pressed;
504
+ };
505
+ if (!modOk(binding.meta, key?.meta)) return false;
506
+ if (!modOk(binding.ctrl, key?.ctrl)) return false;
507
+ if (binding.shift !== void 0 && !!key?.shift !== !!binding.shift) return false;
508
+ return true;
509
+ }
510
+ function formatBindingKey(binding) {
511
+ let label = KEY_LABELS[binding.key] || binding.key;
512
+ if (binding.shift) label = `\u21E7${label}`;
513
+ if (binding.ctrl) label = `^${label}`;
514
+ if (binding.meta) label = `\u2325${label}`;
515
+ return label;
516
+ }
517
+ var KEY_LABELS;
518
+ var init_key_bindings = __esm({
519
+ "src/screen/key-bindings.js"() {
520
+ KEY_LABELS = {
521
+ escape: "esc",
522
+ leftArrow: "\u2190",
523
+ rightArrow: "\u2192",
524
+ upArrow: "\u2191",
525
+ downArrow: "\u2193",
526
+ return: "enter",
527
+ pageUp: "PgUp",
528
+ pageDown: "PgDn"
529
+ };
530
+ }
531
+ });
532
+
480
533
  // src/screen/screens.js
481
534
  import { useState as useState2, createElement as h3 } from "react";
482
535
  import { render, useInput, Text as Text3 } from "ink";
@@ -492,7 +545,7 @@ function groupKeyBindings(bindings) {
492
545
  order: binding.order || 999
493
546
  };
494
547
  }
495
- groups[caption].keys.push(binding.key);
548
+ groups[caption].keys.push(formatBindingKey(binding));
496
549
  });
497
550
  return Object.values(groups);
498
551
  }
@@ -532,12 +585,14 @@ function formatKeyBindings(bindings, mode = "long") {
532
585
  }
533
586
  function formatKeys(keys) {
534
587
  const keyMap = {
535
- "escape": "esc",
536
- "leftArrow": "\u2190",
537
- "rightArrow": "\u2192",
538
- "upArrow": "\u2191",
539
- "downArrow": "\u2193",
540
- "return": "enter"
588
+ escape: "esc",
589
+ leftArrow: "\u2190",
590
+ rightArrow: "\u2192",
591
+ upArrow: "\u2191",
592
+ downArrow: "\u2193",
593
+ return: "enter",
594
+ pageUp: "PgUp",
595
+ pageDown: "PgDn"
541
596
  };
542
597
  return keys.map((k) => keyMap[k] || k).join("/");
543
598
  }
@@ -577,7 +632,10 @@ async function showScreen(config2) {
577
632
  setKeyBinding: (bindingOrBindings) => {
578
633
  const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
579
634
  bindingsToSet.forEach((binding) => {
580
- const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
635
+ const id = bindingIdentity(binding);
636
+ const existingIndex = keyBindings.findIndex(
637
+ (b) => bindingIdentity(b) === id
638
+ );
581
639
  if (existingIndex >= 0) {
582
640
  const existing = keyBindings[existingIndex];
583
641
  if (existing.protected) {
@@ -601,7 +659,10 @@ async function showScreen(config2) {
601
659
  });
602
660
  },
603
661
  updateKeyBinding: (keyName, updates) => {
604
- const index = keyBindings.findIndex((b) => b.key === keyName);
662
+ const id = updates && (updates.meta != null || updates.ctrl != null || updates.shift != null) ? bindingIdentity({ key: keyName, ...updates }) : null;
663
+ const index = keyBindings.findIndex(
664
+ (b) => id ? bindingIdentity(b) === id : b.key === keyName
665
+ );
605
666
  if (index >= 0) {
606
667
  keyBindings[index] = {
607
668
  ...keyBindings[index],
@@ -609,11 +670,13 @@ async function showScreen(config2) {
609
670
  };
610
671
  }
611
672
  },
612
- removeKeyBinding: (keyName) => {
613
- const index = keyBindings.findIndex((b) => b.key === keyName);
673
+ removeKeyBinding: (keyNameOrBinding) => {
674
+ const index = typeof keyNameOrBinding === "object" && keyNameOrBinding ? keyBindings.findIndex(
675
+ (b) => bindingIdentity(b) === bindingIdentity(keyNameOrBinding)
676
+ ) : keyBindings.findIndex((b) => b.key === keyNameOrBinding);
614
677
  if (index >= 0) {
615
678
  if (keyBindings[index].protected) {
616
- console.warn(`Cannot remove protected key: ${keyName}`);
679
+ console.warn(`Cannot remove protected key: ${keyNameOrBinding}`);
617
680
  return;
618
681
  }
619
682
  keyBindings.splice(index, 1);
@@ -654,24 +717,11 @@ async function showScreen(config2) {
654
717
  }
655
718
  let matchedBinding = null;
656
719
  for (const binding of keyBindings) {
657
- let keyMatches = false;
658
- if (key[binding.key]) {
659
- keyMatches = true;
660
- } else if (input === binding.key) {
661
- keyMatches = true;
662
- } else if (key?.name === binding.key) {
663
- keyMatches = true;
664
- }
665
- if (keyMatches) {
666
- if (binding.enabled === false) {
667
- continue;
668
- }
669
- if (binding.condition && !binding.condition(context)) {
670
- continue;
671
- }
672
- matchedBinding = binding;
673
- break;
674
- }
720
+ if (binding.enabled === false) continue;
721
+ if (!bindingMatchesInput(binding, input, key)) continue;
722
+ if (binding.condition && !binding.condition(context)) continue;
723
+ matchedBinding = binding;
724
+ break;
675
725
  }
676
726
  if (matchedBinding && actions[matchedBinding.action]) {
677
727
  actions[matchedBinding.action]({
@@ -807,6 +857,7 @@ var init_screens = __esm({
807
857
  "src/screen/screens.js"() {
808
858
  init_components();
809
859
  init_list_components();
860
+ init_key_bindings();
810
861
  showMenuScreen = showListScreen;
811
862
  showWordGridScreen = showMultiColumnListScreen;
812
863
  }
@@ -896,6 +947,161 @@ var init_ui_elements = __esm({
896
947
  }
897
948
  });
898
949
 
950
+ // src/screen/scrollable-text.js
951
+ import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
952
+ import { Box as Box4, Text as Text5 } from "ink";
953
+ function wrapTextLines(text, cols) {
954
+ const w = Math.max(1, Math.floor(Number(cols) || 1));
955
+ const out = [];
956
+ for (const raw of String(text ?? "").split("\n")) {
957
+ if (raw.length === 0) {
958
+ out.push("");
959
+ continue;
960
+ }
961
+ let rest = raw;
962
+ while (rest.length > w) {
963
+ out.push(rest.slice(0, w));
964
+ rest = rest.slice(w);
965
+ }
966
+ if (rest.length > 0) out.push(rest);
967
+ }
968
+ return out;
969
+ }
970
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
971
+ const view = Math.max(1, Math.floor(viewportRows));
972
+ const total = Math.max(0, Math.floor(totalLines));
973
+ if (total <= view) return null;
974
+ const maxScroll = total - view;
975
+ const thumbSize = Math.max(1, Math.round(view / total * view));
976
+ const travel = Math.max(0, view - thumbSize);
977
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
978
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
979
+ const glyphs = [];
980
+ for (let i = 0; i < view; i++) {
981
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
982
+ }
983
+ return glyphs;
984
+ }
985
+ function padEndVisible(s, w) {
986
+ const t = String(s ?? "");
987
+ if (t.length >= w) return t.slice(0, w);
988
+ return t + " ".repeat(w - t.length);
989
+ }
990
+ function ScrollableText({
991
+ ctx,
992
+ text = "",
993
+ lines: linesProp,
994
+ maxHeight,
995
+ wrap = true,
996
+ showScrollbar = true,
997
+ showStatus = true,
998
+ bindKeys = true,
999
+ header = null
1000
+ }) {
1001
+ const [scrollTop, setScrollTop] = useState3(0);
1002
+ const [, bump] = useState3(0);
1003
+ const termRows = process.stdout.rows || 24;
1004
+ const viewportRows = Math.max(
1005
+ 4,
1006
+ maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
1007
+ );
1008
+ const contentCols = Math.max(20, getScreenWidth() - 4);
1009
+ const barCols = showScrollbar ? 1 : 0;
1010
+ const textWidth = Math.max(8, contentCols - barCols);
1011
+ const allLines = useMemo(() => {
1012
+ if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
1013
+ return wrap ? wrapTextLines(text, textWidth) : String(text ?? "").split("\n");
1014
+ }, [linesProp, text, wrap, textWidth]);
1015
+ const needsBar = showScrollbar && allLines.length > viewportRows;
1016
+ const maxScroll = Math.max(0, allLines.length - viewportRows);
1017
+ const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1018
+ const visible = allLines.slice(clamped, clamped + viewportRows);
1019
+ const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1020
+ useEffect2(() => {
1021
+ setScrollTop((s) => Math.min(s, maxScroll));
1022
+ }, [maxScroll]);
1023
+ const maxScrollRef = useRef2(maxScroll);
1024
+ const pageSizeRef = useRef2(viewportRows);
1025
+ maxScrollRef.current = maxScroll;
1026
+ pageSizeRef.current = viewportRows;
1027
+ useEffect2(() => {
1028
+ if (!ctx || !bindKeys) return void 0;
1029
+ ctx.setKeyBinding(SCROLL_KEYS);
1030
+ ctx.setAction("scrollUp", () => {
1031
+ setScrollTop((s) => Math.max(0, s - 1));
1032
+ bump((n) => n + 1);
1033
+ ctx.update?.();
1034
+ });
1035
+ ctx.setAction("scrollDown", () => {
1036
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1037
+ bump((n) => n + 1);
1038
+ ctx.update?.();
1039
+ });
1040
+ ctx.setAction("pageUp", () => {
1041
+ setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1042
+ bump((n) => n + 1);
1043
+ ctx.update?.();
1044
+ });
1045
+ ctx.setAction("pageDown", () => {
1046
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1047
+ bump((n) => n + 1);
1048
+ ctx.update?.();
1049
+ });
1050
+ return void 0;
1051
+ }, [ctx, bindKeys]);
1052
+ 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" : "");
1053
+ const rowNodes = visible.map((line, i) => {
1054
+ const body = padEndVisible(line, textWidth);
1055
+ const glyph = bar ? bar[i] ?? BAR_TRACK : showScrollbar ? " " : "";
1056
+ const isThumb = glyph === BAR_THUMB;
1057
+ return h5(
1058
+ Text5,
1059
+ { key: `L${clamped + i}` },
1060
+ body,
1061
+ glyph ? h5(Text5, { color: isThumb ? "cyan" : "gray" }, glyph) : null
1062
+ );
1063
+ });
1064
+ if (bar && visible.length < viewportRows) {
1065
+ for (let i = visible.length; i < viewportRows; i++) {
1066
+ const glyph = bar[i] ?? BAR_TRACK;
1067
+ rowNodes.push(
1068
+ h5(
1069
+ Text5,
1070
+ { key: `pad${i}` },
1071
+ padEndVisible("", textWidth),
1072
+ h5(Text5, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
1073
+ )
1074
+ );
1075
+ }
1076
+ }
1077
+ return h5(
1078
+ Box4,
1079
+ { flexDirection: "column" },
1080
+ header == null ? null : typeof header === "string" ? h5(Text5, { color: "gray" }, header) : header,
1081
+ showStatus ? h5(Text5, { color: "gray" }, status) : null,
1082
+ ...rowNodes
1083
+ );
1084
+ }
1085
+ var h5, BAR_THUMB, BAR_TRACK, SCROLL_KEYS;
1086
+ var init_scrollable_text = __esm({
1087
+ "src/screen/scrollable-text.js"() {
1088
+ init_components();
1089
+ h5 = createElement2;
1090
+ BAR_THUMB = "#";
1091
+ BAR_TRACK = "|";
1092
+ SCROLL_KEYS = [
1093
+ { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1094
+ { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1095
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1096
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1097
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1098
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1099
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1100
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1101
+ ];
1102
+ }
1103
+ });
1104
+
899
1105
  // src/screen/utils.js
900
1106
  function buildBreadcrumb(parts) {
901
1107
  if (parts.length === 0) return "";
@@ -1027,7 +1233,7 @@ var init_footer_builder = __esm({
1027
1233
  // src/screen/index.js
1028
1234
  var screen_exports = {};
1029
1235
  __export(screen_exports, {
1030
- Box: () => Box4,
1236
+ Box: () => Box5,
1031
1237
  Divider: () => Divider,
1032
1238
  FooterPresets: () => FooterPresets,
1033
1239
  GridCell: () => GridCell,
@@ -1043,15 +1249,20 @@ __export(screen_exports, {
1043
1249
  ScreenFooter: () => ScreenFooter,
1044
1250
  ScreenRow: () => ScreenRow,
1045
1251
  ScreenTitle: () => ScreenTitle,
1046
- Text: () => Text5,
1252
+ ScrollableText: () => ScrollableText,
1253
+ Text: () => Text6,
1047
1254
  TextBlock: () => TextBlock,
1255
+ bindingIdentity: () => bindingIdentity,
1256
+ bindingMatchesInput: () => bindingMatchesInput,
1048
1257
  buildBreadcrumb: () => buildBreadcrumb,
1049
1258
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1050
1259
  buildFooter: () => buildFooter,
1051
- h: () => createElement2,
1260
+ formatBindingKey: () => formatBindingKey,
1261
+ h: () => createElement3,
1052
1262
  load: () => load,
1053
1263
  memo: () => memo,
1054
1264
  organizeFooterMessages: () => organizeFooterMessages,
1265
+ scrollbarGlyphs: () => scrollbarGlyphs,
1055
1266
  showListScreen: () => showListScreen,
1056
1267
  showMenuScreen: () => showMenuScreen,
1057
1268
  showMultiColumnListScreen: () => showMultiColumnListScreen,
@@ -1059,15 +1270,16 @@ __export(screen_exports, {
1059
1270
  showScreen: () => showScreen,
1060
1271
  showWordGridScreen: () => showWordGridScreen,
1061
1272
  useCallback: () => useCallback,
1062
- useEffect: () => useEffect2,
1273
+ useEffect: () => useEffect3,
1063
1274
  useInput: () => useInput2,
1064
1275
  useLayoutEffect: () => useLayoutEffect,
1065
- useMemo: () => useMemo,
1066
- useRef: () => useRef2,
1067
- useState: () => useState3
1276
+ useMemo: () => useMemo2,
1277
+ useRef: () => useRef3,
1278
+ useState: () => useState4,
1279
+ wrapTextLines: () => wrapTextLines
1068
1280
  });
1069
- import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
1070
- import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
1281
+ import React2, { useState as useState4, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useMemo as useMemo2, useCallback, memo, createElement as createElement3 } from "react";
1282
+ import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
1071
1283
  async function load() {
1072
1284
  if (loadPromise) return loadPromise;
1073
1285
  loadPromise = Promise.all([
@@ -1084,6 +1296,8 @@ var init_screen = __esm({
1084
1296
  init_list_components();
1085
1297
  init_components();
1086
1298
  init_ui_elements();
1299
+ init_scrollable_text();
1300
+ init_key_bindings();
1087
1301
  init_utils();
1088
1302
  init_footer_builder();
1089
1303
  loadPromise = null;