termkit 2.3.1 → 2.4.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/index.js CHANGED
@@ -32,7 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Bar: () => Bar,
34
34
  Chart: () => Chart_exports,
35
- Color: () => import_cosmetic4.default,
35
+ Color: () => import_cosmetic5.default,
36
36
  Column: () => Column,
37
37
  Command: () => Command,
38
38
  Input: () => Input,
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  Program: () => Program,
44
44
  Scrollbox: () => Scrollbox,
45
45
  Select: () => Select,
46
+ Shell: () => Shell,
46
47
  Spinner: () => Spinner,
47
48
  Table: () => Table,
48
49
  TermKit: () => TermKit,
@@ -191,6 +192,8 @@ function findCommand(array, commands) {
191
192
  var RESET = "\x1B[0m";
192
193
  var SHOW_CURSOR = "\x1B[?25h";
193
194
  var HIDE_CURSOR = "\x1B[?25l";
195
+ var DISABLE_WRAP = "\x1B[?7l";
196
+ var ENABLE_WRAP = "\x1B[?7h";
194
197
  var BOLD = "\x1B[1m";
195
198
  var FAINT = "\x1B[2m";
196
199
  var GREEN = "\x1B[32m";
@@ -557,15 +560,22 @@ var Select = class {
557
560
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Select requires an interactive terminal");
558
561
  let selectedIndex = 0;
559
562
  let viewportOffset = 0;
563
+ let visibleStart = 0;
560
564
  let searchQuery = "";
561
565
  let lastDrawnLines = 0;
562
566
  const skipItem = { label: this.skipLabel };
567
+ const computeMaxHeight = () => {
568
+ const termRows = process.stdout.rows ?? 24;
569
+ const reserved = 1 + (this.searchEnabled ? 1 : 0);
570
+ const fit = Math.max(1, termRows - reserved - 1);
571
+ return this.maxHeight ? Math.min(this.maxHeight, fit) : fit;
572
+ };
563
573
  const getFiltered = () => {
564
574
  if (!this.searchEnabled || searchQuery === "") return items;
565
575
  const q = searchQuery.toLowerCase();
566
576
  return items.filter((item) => item.label.toLowerCase().includes(q) || (item.description?.toLowerCase().includes(q) ?? false));
567
577
  };
568
- process.stdout.write(HIDE_CURSOR);
578
+ process.stdout.write(HIDE_CURSOR + DISABLE_WRAP);
569
579
  const glyph = this.promptGlyph ? `${colorText(this.promptColor, this.promptGlyph)} ` : "";
570
580
  const indent = " ".repeat(this.promptGlyph ? stringLength(this.promptGlyph) + 1 : 0);
571
581
  process.stdout.write(`${glyph}${prompt}
@@ -574,13 +584,17 @@ var Select = class {
574
584
  const filtered = getFiltered();
575
585
  const allItems = [...filtered, skipItem];
576
586
  if (selectedIndex >= allItems.length) selectedIndex = allItems.length - 1;
577
- if (this.maxHeight) {
587
+ const maxHeight = computeMaxHeight();
588
+ const useViewport = allItems.length > maxHeight;
589
+ if (useViewport) {
578
590
  if (selectedIndex < viewportOffset) viewportOffset = selectedIndex;
579
- else if (selectedIndex >= viewportOffset + this.maxHeight) viewportOffset = selectedIndex - this.maxHeight + 1;
580
- viewportOffset = Math.max(0, Math.min(viewportOffset, Math.max(0, allItems.length - this.maxHeight)));
591
+ else if (selectedIndex >= viewportOffset + maxHeight) viewportOffset = selectedIndex - maxHeight + 1;
592
+ viewportOffset = Math.max(0, Math.min(viewportOffset, Math.max(0, allItems.length - maxHeight)));
593
+ } else {
594
+ viewportOffset = 0;
581
595
  }
582
- const visibleStart = this.maxHeight ? viewportOffset : 0;
583
- const visibleEnd = this.maxHeight ? Math.min(allItems.length, viewportOffset + this.maxHeight) : allItems.length;
596
+ visibleStart = useViewport ? viewportOffset : 0;
597
+ const visibleEnd = useViewport ? Math.min(allItems.length, viewportOffset + maxHeight) : allItems.length;
584
598
  if (redraw) {
585
599
  if (lastDrawnLines > 0) process.stdout.write(CURSOR_UP(lastDrawnLines));
586
600
  process.stdout.write("\r\x1B[0J");
@@ -596,8 +610,7 @@ var Select = class {
596
610
  const item = allItems[i];
597
611
  const isSelected = i === selectedIndex;
598
612
  const isSkip = i === filtered.length;
599
- const relativeNum = i - visibleStart + 1;
600
- const numStr = isSkip ? "0." : `${relativeNum}.`;
613
+ const numStr = isSkip ? "0." : `${i + 1}.`;
601
614
  const desc = item.description ? ` ${colorText(this.descriptionColor, `\u2014 ${item.description}`)}` : "";
602
615
  let marker, tail;
603
616
  if (isSelected && pulse) {
@@ -624,7 +637,7 @@ var Select = class {
624
637
  process.stdin.setRawMode(false);
625
638
  process.stdin.pause();
626
639
  process.stdin.removeListener("data", onKey);
627
- process.stdout.write(SHOW_CURSOR);
640
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
628
641
  });
629
642
  const cleanup = () => {
630
643
  deregisterCleanup();
@@ -635,7 +648,7 @@ var Select = class {
635
648
  process.stdin.setRawMode(false);
636
649
  process.stdin.pause();
637
650
  process.stdin.removeListener("data", onKey);
638
- process.stdout.write(SHOW_CURSOR);
651
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
639
652
  };
640
653
  if (this._parsedColors.length >= 2) {
641
654
  timer = setInterval(() => {
@@ -678,8 +691,7 @@ var Select = class {
678
691
  } else {
679
692
  const n = parseInt(str);
680
693
  if (!isNaN(n) && n >= 0 && n <= Math.min(items.length, 9)) {
681
- const visibleStart = this.maxHeight ? viewportOffset : 0;
682
- selectedIndex = n === 0 ? allItems.length - 1 : visibleStart + n - 1;
694
+ selectedIndex = n === 0 ? allItems.length - 1 : n - 1;
683
695
  selectedIndex = Math.max(0, Math.min(selectedIndex, allItems.length - 1));
684
696
  renderList(true);
685
697
  }
@@ -860,7 +872,7 @@ var Command = class {
860
872
  constructor(data) {
861
873
  this.actionFunction = null;
862
874
  this.commandsArray = [];
863
- this.commandStrings = ["help"];
875
+ this.commandStrings = ["help", "version"];
864
876
  this.info = null;
865
877
  this.middlewaresArray = [];
866
878
  this.name = null;
@@ -930,6 +942,9 @@ var Command = class {
930
942
  const recursive = source?.includes("-r") === true || source?.includes("--recursive") === true;
931
943
  this.printHelp(this.name ?? "Program", recursive);
932
944
  }
945
+ printVersion() {
946
+ console.log(this.versionString ?? "");
947
+ }
933
948
  printHelp(fullName, recursive) {
934
949
  const table = [];
935
950
  let program = fullName;
@@ -989,29 +1004,32 @@ var Command = class {
989
1004
  }
990
1005
  }
991
1006
  }
992
- async parse(input2) {
993
- const array = [...input2];
994
- array.splice(0, 2);
1007
+ async _execute(tokens) {
1008
+ const array = [...tokens];
995
1009
  let command = this;
996
1010
  const options = { _source: Array.from(array) };
1011
+ if (this.versionString && (array.includes("--version") || array.includes("-V"))) {
1012
+ return this.printVersion();
1013
+ }
997
1014
  const ddIdx = array.indexOf("--");
998
1015
  if (ddIdx !== -1) {
999
1016
  options._ = array.splice(ddIdx + 1);
1000
1017
  array.splice(ddIdx, 1);
1001
1018
  }
1002
1019
  while (array.length) {
1003
- if (!array.includes("help")) {
1020
+ if (!array.includes("help") && !array.includes("version")) {
1004
1021
  Object.assign(options, await findOptions(array, command));
1005
1022
  const cmdVars = await findCommandVariables(array, command);
1006
1023
  if (cmdVars) Object.assign(options, cmdVars);
1007
1024
  Object.assign(options, await findOptions(array, command));
1008
1025
  }
1009
1026
  if (array.length) {
1010
- if (!array.includes("help")) {
1027
+ if (!array.includes("help") && !array.includes("version")) {
1011
1028
  for (const mw of command.middlewaresArray) await mw(options);
1012
1029
  }
1013
1030
  const next = findCommand(array, command.commandsArray);
1014
1031
  if (!next && array[0] === "help") return command.help(options._source);
1032
+ if (!next && array[0] === "version") return this.printVersion();
1015
1033
  if (!next) throw new SyntaxError(`Unknown command: ${array[0]}`);
1016
1034
  const name = command.name ?? "_base";
1017
1035
  if (!options._parents) options._parents = {};
@@ -1035,11 +1053,226 @@ var Command = class {
1035
1053
  }
1036
1054
  for (const mw of command.middlewaresArray) await mw(options);
1037
1055
  if (command.actionFunction) return command.actionFunction(options);
1038
- if (options._source.length === 2) return command.help(options._source);
1039
- throw new Error(`No action for command: ${command.name ?? "_base"}`);
1056
+ return command.help(options._source);
1057
+ }
1058
+ async parse(input2) {
1059
+ return this._execute(input2.slice(2));
1040
1060
  }
1041
1061
  };
1042
1062
 
1063
+ // src/models/Shell.ts
1064
+ var import_cosmetic2 = __toESM(require("cosmetic"));
1065
+ var readline = __toESM(require("readline"));
1066
+ var Shell = class {
1067
+ constructor(root, opts = {}) {
1068
+ this.rl = null;
1069
+ this.root = root;
1070
+ this.opts = {
1071
+ mode: opts.mode ?? "drill",
1072
+ prompt: opts.prompt ?? root.name ?? "shell",
1073
+ promptColor: opts.promptColor ?? "",
1074
+ banner: opts.banner ?? "",
1075
+ exitCommands: opts.exitCommands ?? ["exit", "quit"],
1076
+ historySize: opts.historySize ?? 100
1077
+ };
1078
+ }
1079
+ async run() {
1080
+ this.rl = readline.createInterface({
1081
+ input: process.stdin,
1082
+ output: process.stdout,
1083
+ terminal: true,
1084
+ historySize: this.opts.historySize,
1085
+ completer: this.opts.mode === "free" ? makeCompleter(this.root) : void 0
1086
+ });
1087
+ if (this.opts.banner) console.log(this.opts.banner);
1088
+ try {
1089
+ if (this.opts.mode === "free") {
1090
+ await this.freeLoop();
1091
+ } else {
1092
+ await this.drillLoop();
1093
+ }
1094
+ } finally {
1095
+ this.rl.close();
1096
+ }
1097
+ }
1098
+ // ── Drill mode ────────────────────────────────────────────────────────
1099
+ async drillLoop() {
1100
+ while (true) {
1101
+ const exited = await this.drillFrom(this.root, [this.root.name ?? "shell"]);
1102
+ if (exited) return;
1103
+ }
1104
+ }
1105
+ async drillFrom(cmd, breadcrumb) {
1106
+ while (true) {
1107
+ const token = await this.promptDrill(cmd, breadcrumb);
1108
+ if (token === null) {
1109
+ return breadcrumb.length === 1;
1110
+ }
1111
+ if (this.opts.exitCommands.includes(token)) process.exit(0);
1112
+ if (token === "..") return false;
1113
+ if (token === "help") {
1114
+ cmd.help();
1115
+ continue;
1116
+ }
1117
+ const sub = cmd.commandsArray.find((c) => c.name === token);
1118
+ if (!sub) {
1119
+ process.stderr.write(`Unknown command: ${token}
1120
+ `);
1121
+ continue;
1122
+ }
1123
+ if (sub.commandsArray.length > 0) {
1124
+ const exited = await this.drillFrom(sub, [...breadcrumb, sub.name ?? ""]);
1125
+ if (exited) return true;
1126
+ continue;
1127
+ }
1128
+ const vars = await this.gatherVariables(sub);
1129
+ const tokens = buildTokens(sub, vars);
1130
+ try {
1131
+ await sub._execute(tokens);
1132
+ } catch (err) {
1133
+ process.stderr.write(`${err instanceof Error ? err.message : err}
1134
+ `);
1135
+ }
1136
+ return false;
1137
+ }
1138
+ }
1139
+ async promptDrill(cmd, breadcrumb) {
1140
+ const subs = cmd.commandsArray.map((c) => c.name ?? "").filter(Boolean);
1141
+ const label = this.colorize(breadcrumb.join(" "));
1142
+ const choices = [...subs, "help"].join(", ");
1143
+ process.stdout.write(`
1144
+ ${label} ${choices}
1145
+ `);
1146
+ return new Promise((resolve) => {
1147
+ let resolved = false;
1148
+ const done = (val) => {
1149
+ if (!resolved) {
1150
+ resolved = true;
1151
+ resolve(val);
1152
+ }
1153
+ };
1154
+ this.rl.question("> ", (answer) => done(answer.trim() || null));
1155
+ this.rl.once("close", () => done(null));
1156
+ });
1157
+ }
1158
+ async gatherVariables(cmd) {
1159
+ const result = {};
1160
+ if (!cmd.variables) return result;
1161
+ for (const v of cmd.variables) {
1162
+ const name = v.name ?? "value";
1163
+ const hint = v.hint ? ` ${v.hint}` : "";
1164
+ while (true) {
1165
+ const answer = await new Promise((resolve) => {
1166
+ let resolved = false;
1167
+ const done = (val) => {
1168
+ if (!resolved) {
1169
+ resolved = true;
1170
+ resolve(val);
1171
+ }
1172
+ };
1173
+ this.rl.question(` ${name}${hint}: `, (ans) => done(ans.trim() || null));
1174
+ this.rl.once("close", () => done(null));
1175
+ });
1176
+ const value = answer ?? v.default ?? null;
1177
+ if (!value && v.required) {
1178
+ process.stderr.write(` ${name} is required
1179
+ `);
1180
+ continue;
1181
+ }
1182
+ if (value) {
1183
+ if (v.type === "enum" && v.enum && !v.enum.includes(value)) {
1184
+ process.stderr.write(` Must be one of: ${v.enum.join(", ")}
1185
+ `);
1186
+ continue;
1187
+ }
1188
+ result[name] = value;
1189
+ }
1190
+ break;
1191
+ }
1192
+ }
1193
+ return result;
1194
+ }
1195
+ // ── Free mode ─────────────────────────────────────────────────────────
1196
+ async freeLoop() {
1197
+ const prompt = `${this.colorize(this.opts.prompt)} > `;
1198
+ this.rl.setPrompt(prompt);
1199
+ this.rl.prompt();
1200
+ for await (const line of this.rl) {
1201
+ const trimmed = line.trim();
1202
+ if (!trimmed) {
1203
+ this.rl.prompt();
1204
+ continue;
1205
+ }
1206
+ if (this.opts.exitCommands.includes(trimmed)) break;
1207
+ const tokens = tokenize(trimmed);
1208
+ try {
1209
+ await this.root._execute(tokens);
1210
+ } catch (err) {
1211
+ process.stderr.write(`${err instanceof Error ? err.message : err}
1212
+ `);
1213
+ }
1214
+ this.rl.prompt();
1215
+ }
1216
+ }
1217
+ // ── Helpers ───────────────────────────────────────────────────────────
1218
+ colorize(text) {
1219
+ const c = this.opts.promptColor;
1220
+ if (!c || !process.stdout.isTTY) return text;
1221
+ try {
1222
+ if (c.startsWith("#")) return import_cosmetic2.default.hex(c).encoder(text);
1223
+ if (/^\d+$/.test(c)) return import_cosmetic2.default.xterm(Number(c)).encoder(text);
1224
+ const style = import_cosmetic2.default[c];
1225
+ if (style && typeof style.encoder === "function") return style.encoder(text);
1226
+ } catch {
1227
+ }
1228
+ return text;
1229
+ }
1230
+ };
1231
+ function tokenize(line) {
1232
+ const tokens = [];
1233
+ let current = "";
1234
+ let inQuote = null;
1235
+ for (const ch of line) {
1236
+ if (inQuote) {
1237
+ if (ch === inQuote) {
1238
+ inQuote = null;
1239
+ } else {
1240
+ current += ch;
1241
+ }
1242
+ } else if (ch === '"' || ch === "'") {
1243
+ inQuote = ch;
1244
+ } else if (ch === " " || ch === " ") {
1245
+ if (current) {
1246
+ tokens.push(current);
1247
+ current = "";
1248
+ }
1249
+ } else {
1250
+ current += ch;
1251
+ }
1252
+ }
1253
+ if (current) tokens.push(current);
1254
+ return tokens;
1255
+ }
1256
+ function buildTokens(cmd, vars) {
1257
+ if (!cmd.variables) return [];
1258
+ return cmd.variables.map((v) => vars[v.name ?? ""]).filter((v) => v !== void 0 && v !== "");
1259
+ }
1260
+ function makeCompleter(root) {
1261
+ return (line) => {
1262
+ const tokens = tokenize(line);
1263
+ let cmd = root;
1264
+ for (const token of tokens.slice(0, -1)) {
1265
+ const sub = cmd.commandsArray.find((c) => c.name === token);
1266
+ if (!sub) break;
1267
+ cmd = sub;
1268
+ }
1269
+ const partial = tokens[tokens.length - 1] ?? "";
1270
+ const names = [...cmd.commandsArray.map((c) => c.name ?? "").filter(Boolean), "help"];
1271
+ const hits = names.filter((n) => n.startsWith(partial));
1272
+ return [hits.length ? hits : names, partial];
1273
+ };
1274
+ }
1275
+
1043
1276
  // src/models/Bar.ts
1044
1277
  var _Bar = class _Bar {
1045
1278
  constructor(text, options) {
@@ -1500,7 +1733,7 @@ __export(Chart_exports, {
1500
1733
  Sparkline: () => Sparkline,
1501
1734
  VerticalBar: () => VerticalBar
1502
1735
  });
1503
- var import_cosmetic2 = __toESM(require("cosmetic"));
1736
+ var import_cosmetic3 = __toESM(require("cosmetic"));
1504
1737
 
1505
1738
  // src/utils/padLeft.ts
1506
1739
  var padLeft = (string, padding) => {
@@ -1521,9 +1754,9 @@ function formatNum(n) {
1521
1754
  }
1522
1755
  function applyConfigColor(s) {
1523
1756
  const c = config.color;
1524
- if (typeof c === "number") return import_cosmetic2.default.xterm(c).encoder(s);
1525
- if (c.startsWith("#")) return import_cosmetic2.default.hex(c).encoder(s);
1526
- return import_cosmetic2.default[c].encoder(s);
1757
+ if (typeof c === "number") return import_cosmetic3.default.xterm(c).encoder(s);
1758
+ if (c.startsWith("#")) return import_cosmetic3.default.hex(c).encoder(s);
1759
+ return import_cosmetic3.default[c].encoder(s);
1527
1760
  }
1528
1761
  function applyPadding(str, paddingX, paddingY) {
1529
1762
  const lines = str.split("\n");
@@ -1549,7 +1782,7 @@ var Bar2 = class {
1549
1782
  const cols = options.width ?? process.stdout.columns ?? 80;
1550
1783
  const available = cols - maxKeyLen - maxValueLen - 3;
1551
1784
  const scale = maxValue > 0 && available > 0 ? available / maxValue : 0;
1552
- const encodeKey = (s) => typeof config.color === "number" ? import_cosmetic2.default.xterm(config.color).encoder(s) : config.color.startsWith("#") ? import_cosmetic2.default.hex(config.color).encoder(s) : import_cosmetic2.default[config.color].encoder(s);
1785
+ const encodeKey = (s) => typeof config.color === "number" ? import_cosmetic3.default.xterm(config.color).encoder(s) : config.color.startsWith("#") ? import_cosmetic3.default.hex(config.color).encoder(s) : import_cosmetic3.default[config.color].encoder(s);
1553
1786
  for (const item of data) {
1554
1787
  if (!item) {
1555
1788
  this.string += "\n";
@@ -1559,7 +1792,7 @@ var Bar2 = class {
1559
1792
  const keyPart = (rawKey ? encodeKey(rawKey) : "") + " ".repeat(maxKeyLen - stringLength(rawKey));
1560
1793
  const barWidth = Math.max(1, Math.floor(item.value * scale));
1561
1794
  let bar = (item.character ?? " ").repeat(barWidth);
1562
- bar = item.style ? item.style(bar) : import_cosmetic2.default.background.white.encoder(bar);
1795
+ bar = item.style ? item.style(bar) : import_cosmetic3.default.background.white.encoder(bar);
1563
1796
  this.string += `${keyPart}|${bar} ${item.value}
1564
1797
  `;
1565
1798
  }
@@ -1608,7 +1841,7 @@ var VerticalBar = class {
1608
1841
  }
1609
1842
  this.string += "\u2500".repeat(data.length * colWidth) + "\n";
1610
1843
  if (data.some((item) => item?.key)) {
1611
- const encodeLabel = (s) => typeof config.color === "number" ? import_cosmetic2.default.xterm(config.color).encoder(s) : config.color.startsWith("#") ? import_cosmetic2.default.hex(config.color).encoder(s) : import_cosmetic2.default[config.color].encoder(s);
1844
+ const encodeLabel = (s) => typeof config.color === "number" ? import_cosmetic3.default.xterm(config.color).encoder(s) : config.color.startsWith("#") ? import_cosmetic3.default.hex(config.color).encoder(s) : import_cosmetic3.default[config.color].encoder(s);
1612
1845
  let labels = "";
1613
1846
  for (const item of data) {
1614
1847
  if (!item?.key) {
@@ -2108,7 +2341,7 @@ var MultiSelect = class {
2108
2341
  const checked = /* @__PURE__ */ new Set();
2109
2342
  let error = null;
2110
2343
  let lastDrawnLines = 0;
2111
- process.stdout.write(HIDE_CURSOR);
2344
+ process.stdout.write(HIDE_CURSOR + DISABLE_WRAP);
2112
2345
  const glyph = this.promptGlyph ? `${colorText(this.promptColor, this.promptGlyph)} ` : "";
2113
2346
  const indent = " ".repeat(this.promptGlyph ? stringLength(this.promptGlyph) + 1 : 0);
2114
2347
  process.stdout.write(`${glyph}${prompt}
@@ -2118,16 +2351,26 @@ var MultiSelect = class {
2118
2351
  const q = searchQuery.toLowerCase();
2119
2352
  return items.map((item, i) => ({ item, originalIndex: i })).filter(({ item }) => item.label.toLowerCase().includes(q) || (item.description?.toLowerCase().includes(q) ?? false));
2120
2353
  };
2354
+ const computeMaxHeight = () => {
2355
+ const termRows = process.stdout.rows ?? 24;
2356
+ const reserved = 1 + (this.searchEnabled ? 1 : 0) + 1 + 1;
2357
+ const fit = Math.max(1, termRows - reserved - 1);
2358
+ return this.maxHeight ? Math.min(this.maxHeight, fit) : fit;
2359
+ };
2121
2360
  const renderList = (redraw) => {
2122
2361
  const filtered = getFiltered();
2123
2362
  if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
2124
- if (this.maxHeight) {
2363
+ const maxHeight = computeMaxHeight();
2364
+ const useViewport = filtered.length > maxHeight;
2365
+ if (useViewport) {
2125
2366
  if (cursor < viewportOffset) viewportOffset = cursor;
2126
- else if (cursor >= viewportOffset + this.maxHeight) viewportOffset = cursor - this.maxHeight + 1;
2127
- viewportOffset = Math.max(0, Math.min(viewportOffset, Math.max(0, filtered.length - this.maxHeight)));
2367
+ else if (cursor >= viewportOffset + maxHeight) viewportOffset = cursor - maxHeight + 1;
2368
+ viewportOffset = Math.max(0, Math.min(viewportOffset, Math.max(0, filtered.length - maxHeight)));
2369
+ } else {
2370
+ viewportOffset = 0;
2128
2371
  }
2129
- const visibleStart = this.maxHeight ? viewportOffset : 0;
2130
- const visibleEnd = this.maxHeight ? Math.min(filtered.length, viewportOffset + this.maxHeight) : filtered.length;
2372
+ const visibleStart = useViewport ? viewportOffset : 0;
2373
+ const visibleEnd = useViewport ? Math.min(filtered.length, viewportOffset + maxHeight) : filtered.length;
2131
2374
  if (redraw) {
2132
2375
  if (lastDrawnLines > 0) process.stdout.write(CURSOR_UP3(lastDrawnLines));
2133
2376
  process.stdout.write("\r\x1B[0J");
@@ -2143,14 +2386,15 @@ var MultiSelect = class {
2143
2386
  const { item, originalIndex } = filtered[vi];
2144
2387
  const isCursor = vi === cursor;
2145
2388
  const isChecked = checked.has(originalIndex);
2389
+ const numStr = `${vi + 1}.`;
2146
2390
  const desc = item.description ? ` ${colorText(this.descriptionColor, `\u2014 ${item.description}`)}` : "";
2147
2391
  const checkMark = isChecked ? pulse ? `${pulse}${this.checkedPrefix}${RESET}` : colorText(this.promptColor, this.checkedPrefix) : this.uncheckedPrefix;
2148
2392
  const label = isCursor ? pulse ? `${pulse}${item.label}${RESET}` : colorText(this.promptColor, item.label) : item.label;
2149
- process.stdout.write(`\r${indent}${checkMark} ${label}${desc}
2393
+ process.stdout.write(`\r${indent}${numStr} ${checkMark} ${label}${desc}
2150
2394
  `);
2151
2395
  lastDrawnLines++;
2152
2396
  }
2153
- const hintContent = `\u2191\u2193 move space/tab toggle \u2190\u2192 deselect/select${this.searchEnabled ? " type to filter" : " a all"} enter confirm`;
2397
+ const hintContent = `\u2191\u2193 move space/tab toggle \u2190\u2192 deselect/select${this.searchEnabled ? " type to filter" : " 1-9 jump a all"} enter confirm`;
2154
2398
  const maxHintCols = Math.max(10, (process.stdout.columns ?? 80) - stringLength(indent) - 1);
2155
2399
  const hint = stringLength(hintContent) > maxHintCols ? hintContent.slice(0, maxHintCols) : hintContent;
2156
2400
  process.stdout.write(`\r${indent}${DIM2}${hint}${RESET}
@@ -2173,7 +2417,7 @@ var MultiSelect = class {
2173
2417
  process.stdin.setRawMode(false);
2174
2418
  process.stdin.pause();
2175
2419
  process.stdin.removeListener("data", onKey);
2176
- process.stdout.write(SHOW_CURSOR);
2420
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
2177
2421
  });
2178
2422
  const cleanup = (result) => {
2179
2423
  deregisterCleanup();
@@ -2183,6 +2427,7 @@ var MultiSelect = class {
2183
2427
  }
2184
2428
  if (lastDrawnLines > 0) process.stdout.write(CURSOR_UP3(lastDrawnLines));
2185
2429
  process.stdout.write("\x1B[0J");
2430
+ process.stdout.write(ENABLE_WRAP);
2186
2431
  const bulletWidth = stringLength(this.checkedPrefix) + 1;
2187
2432
  for (let i = 0; i < items.length; i++) {
2188
2433
  const item = items[i];
@@ -2194,7 +2439,7 @@ var MultiSelect = class {
2194
2439
  process.stdin.setRawMode(false);
2195
2440
  process.stdin.pause();
2196
2441
  process.stdin.removeListener("data", onKey);
2197
- process.stdout.write(SHOW_CURSOR);
2442
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
2198
2443
  resolve(result);
2199
2444
  };
2200
2445
  if (this._parsedColors.length >= 2) {
@@ -2283,7 +2528,7 @@ var MultiSelect = class {
2283
2528
  process.stdin.setRawMode(false);
2284
2529
  process.stdin.pause();
2285
2530
  process.stdin.removeListener("data", onKey);
2286
- process.stdout.write(SHOW_CURSOR);
2531
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
2287
2532
  process.exit(130);
2288
2533
  } else if (this.searchEnabled) {
2289
2534
  if (str === "\x7F" || str === "\b") {
@@ -2303,7 +2548,7 @@ var MultiSelect = class {
2303
2548
  const n = parseInt(str);
2304
2549
  if (!isNaN(n) && n >= 1 && n <= Math.min(filtered.length, 9)) {
2305
2550
  error = null;
2306
- cursor = viewportOffset + n - 1;
2551
+ cursor = n - 1;
2307
2552
  renderList(true);
2308
2553
  }
2309
2554
  }
@@ -2677,7 +2922,7 @@ _Spinner.FRAMES = {
2677
2922
  var Spinner = _Spinner;
2678
2923
 
2679
2924
  // src/models/Table.ts
2680
- var import_cosmetic3 = __toESM(require("cosmetic"));
2925
+ var import_cosmetic4 = __toESM(require("cosmetic"));
2681
2926
 
2682
2927
  // src/utils/padSides.ts
2683
2928
  var padSides = (string, padding) => {
@@ -2755,7 +3000,7 @@ var Table = class {
2755
3000
  header += pad(column.title, column.padding + this.margin);
2756
3001
  if (i < keys.length - 1) header += this.separator;
2757
3002
  }
2758
- const styled = typeof config.color === "number" ? import_cosmetic3.default.xterm(config.color) : config.color.startsWith("#") ? import_cosmetic3.default.hex(config.color) : import_cosmetic3.default[config.color];
3003
+ const styled = typeof config.color === "number" ? import_cosmetic4.default.xterm(config.color) : config.color.startsWith("#") ? import_cosmetic4.default.hex(config.color) : import_cosmetic4.default[config.color];
2759
3004
  this.string += `${styled.underline.encoder(header)}
2760
3005
  `;
2761
3006
  for (const [ri, row] of this.rows.entries()) {
@@ -2826,13 +3071,13 @@ _TermKit.commandDefaults = {};
2826
3071
  var TermKit = _TermKit;
2827
3072
 
2828
3073
  // src/index.ts
2829
- var import_cosmetic4 = __toESM(require("cosmetic"));
3074
+ var import_cosmetic5 = __toESM(require("cosmetic"));
2830
3075
  var base = null;
2831
3076
  var commandDefaults = {};
2832
3077
  var Program = {
2833
3078
  command: (name, variables, info) => {
2834
3079
  const cmd = new Command(Object.assign({ name, variables, info }, commandDefaults));
2835
- if (!base) base = cmd;
3080
+ base = cmd;
2836
3081
  return cmd;
2837
3082
  },
2838
3083
  option: (short, long, variables, info) => new Option({ short, long, variables, info }),
@@ -2855,6 +3100,10 @@ var Program = {
2855
3100
  },
2856
3101
  setDefaults: (data) => {
2857
3102
  commandDefaults = data;
3103
+ },
3104
+ shell: async (options) => {
3105
+ if (!base) throw new Error("No command defined");
3106
+ return new Shell(base, options).run();
2858
3107
  }
2859
3108
  };
2860
3109
  // Annotate the CommonJS export names for ESM import in node:
@@ -2872,6 +3121,7 @@ var Program = {
2872
3121
  Program,
2873
3122
  Scrollbox,
2874
3123
  Select,
3124
+ Shell,
2875
3125
  Spinner,
2876
3126
  Table,
2877
3127
  TermKit,