termkit 2.3.1 → 2.4.1

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,9 +637,9 @@ 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
- const cleanup = () => {
642
+ const cleanup = (selectedLabel) => {
630
643
  deregisterCleanup();
631
644
  if (timer) {
632
645
  clearInterval(timer);
@@ -635,6 +648,11 @@ var Select = class {
635
648
  process.stdin.setRawMode(false);
636
649
  process.stdin.pause();
637
650
  process.stdin.removeListener("data", onKey);
651
+ process.stdout.write(CURSOR_UP(lastDrawnLines + 1));
652
+ process.stdout.write("\r\x1B[0J");
653
+ process.stdout.write(ENABLE_WRAP);
654
+ process.stdout.write(`${glyph}${prompt}: ${selectedLabel ?? this.skipLabel}
655
+ `);
638
656
  process.stdout.write(SHOW_CURSOR);
639
657
  };
640
658
  if (this._parsedColors.length >= 2) {
@@ -657,10 +675,19 @@ var Select = class {
657
675
  selectedIndex = (selectedIndex + 1) % allItems.length;
658
676
  renderList(true);
659
677
  } else if (str === "\r" || str === "\n") {
660
- cleanup();
661
- resolve(selectedIndex === filtered.length ? null : filtered[selectedIndex] ?? null);
678
+ const result = selectedIndex === filtered.length ? null : filtered[selectedIndex] ?? null;
679
+ cleanup(result?.label ?? null);
680
+ resolve(result);
662
681
  } else if (str === "") {
663
- cleanup();
682
+ deregisterCleanup();
683
+ if (timer) {
684
+ clearInterval(timer);
685
+ timer = null;
686
+ }
687
+ process.stdin.setRawMode(false);
688
+ process.stdin.pause();
689
+ process.stdin.removeListener("data", onKey);
690
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
664
691
  process.exit(130);
665
692
  } else if (this.searchEnabled) {
666
693
  if (str === "\x7F" || str === "\b") {
@@ -678,8 +705,7 @@ var Select = class {
678
705
  } else {
679
706
  const n = parseInt(str);
680
707
  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;
708
+ selectedIndex = n === 0 ? allItems.length - 1 : n - 1;
683
709
  selectedIndex = Math.max(0, Math.min(selectedIndex, allItems.length - 1));
684
710
  renderList(true);
685
711
  }
@@ -860,7 +886,7 @@ var Command = class {
860
886
  constructor(data) {
861
887
  this.actionFunction = null;
862
888
  this.commandsArray = [];
863
- this.commandStrings = ["help"];
889
+ this.commandStrings = ["help", "version"];
864
890
  this.info = null;
865
891
  this.middlewaresArray = [];
866
892
  this.name = null;
@@ -930,6 +956,9 @@ var Command = class {
930
956
  const recursive = source?.includes("-r") === true || source?.includes("--recursive") === true;
931
957
  this.printHelp(this.name ?? "Program", recursive);
932
958
  }
959
+ printVersion() {
960
+ console.log(this.versionString ?? "");
961
+ }
933
962
  printHelp(fullName, recursive) {
934
963
  const table = [];
935
964
  let program = fullName;
@@ -989,29 +1018,32 @@ var Command = class {
989
1018
  }
990
1019
  }
991
1020
  }
992
- async parse(input2) {
993
- const array = [...input2];
994
- array.splice(0, 2);
1021
+ async _execute(tokens) {
1022
+ const array = [...tokens];
995
1023
  let command = this;
996
1024
  const options = { _source: Array.from(array) };
1025
+ if (this.versionString && (array.includes("--version") || array.includes("-V"))) {
1026
+ return this.printVersion();
1027
+ }
997
1028
  const ddIdx = array.indexOf("--");
998
1029
  if (ddIdx !== -1) {
999
1030
  options._ = array.splice(ddIdx + 1);
1000
1031
  array.splice(ddIdx, 1);
1001
1032
  }
1002
1033
  while (array.length) {
1003
- if (!array.includes("help")) {
1034
+ if (!array.includes("help") && !array.includes("version")) {
1004
1035
  Object.assign(options, await findOptions(array, command));
1005
1036
  const cmdVars = await findCommandVariables(array, command);
1006
1037
  if (cmdVars) Object.assign(options, cmdVars);
1007
1038
  Object.assign(options, await findOptions(array, command));
1008
1039
  }
1009
1040
  if (array.length) {
1010
- if (!array.includes("help")) {
1041
+ if (!array.includes("help") && !array.includes("version")) {
1011
1042
  for (const mw of command.middlewaresArray) await mw(options);
1012
1043
  }
1013
1044
  const next = findCommand(array, command.commandsArray);
1014
1045
  if (!next && array[0] === "help") return command.help(options._source);
1046
+ if (!next && array[0] === "version") return this.printVersion();
1015
1047
  if (!next) throw new SyntaxError(`Unknown command: ${array[0]}`);
1016
1048
  const name = command.name ?? "_base";
1017
1049
  if (!options._parents) options._parents = {};
@@ -1035,10 +1067,225 @@ var Command = class {
1035
1067
  }
1036
1068
  for (const mw of command.middlewaresArray) await mw(options);
1037
1069
  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"}`);
1070
+ return command.help(options._source);
1071
+ }
1072
+ async parse(input2) {
1073
+ return this._execute(input2.slice(2));
1074
+ }
1075
+ };
1076
+
1077
+ // src/models/Shell.ts
1078
+ var import_cosmetic2 = __toESM(require("cosmetic"));
1079
+ var readline = __toESM(require("readline"));
1080
+ var Shell = class {
1081
+ constructor(root, opts = {}) {
1082
+ this.rl = null;
1083
+ this.root = root;
1084
+ this.opts = {
1085
+ mode: opts.mode ?? "drill",
1086
+ prompt: opts.prompt ?? root.name ?? "shell",
1087
+ promptColor: opts.promptColor ?? "",
1088
+ banner: opts.banner ?? "",
1089
+ exitCommands: opts.exitCommands ?? ["exit", "quit"],
1090
+ historySize: opts.historySize ?? 100
1091
+ };
1092
+ }
1093
+ async run() {
1094
+ this.rl = readline.createInterface({
1095
+ input: process.stdin,
1096
+ output: process.stdout,
1097
+ terminal: true,
1098
+ historySize: this.opts.historySize,
1099
+ completer: this.opts.mode === "free" ? makeCompleter(this.root) : void 0
1100
+ });
1101
+ if (this.opts.banner) console.log(this.opts.banner);
1102
+ try {
1103
+ if (this.opts.mode === "free") {
1104
+ await this.freeLoop();
1105
+ } else {
1106
+ await this.drillLoop();
1107
+ }
1108
+ } finally {
1109
+ this.rl.close();
1110
+ }
1111
+ }
1112
+ // ── Drill mode ────────────────────────────────────────────────────────
1113
+ async drillLoop() {
1114
+ while (true) {
1115
+ const exited = await this.drillFrom(this.root, [this.root.name ?? "shell"]);
1116
+ if (exited) return;
1117
+ }
1118
+ }
1119
+ async drillFrom(cmd, breadcrumb) {
1120
+ while (true) {
1121
+ const token = await this.promptDrill(cmd, breadcrumb);
1122
+ if (token === null) {
1123
+ return breadcrumb.length === 1;
1124
+ }
1125
+ if (this.opts.exitCommands.includes(token)) process.exit(0);
1126
+ if (token === "..") return false;
1127
+ if (token === "help") {
1128
+ cmd.help();
1129
+ continue;
1130
+ }
1131
+ const sub = cmd.commandsArray.find((c) => c.name === token);
1132
+ if (!sub) {
1133
+ process.stderr.write(`Unknown command: ${token}
1134
+ `);
1135
+ continue;
1136
+ }
1137
+ if (sub.commandsArray.length > 0) {
1138
+ const exited = await this.drillFrom(sub, [...breadcrumb, sub.name ?? ""]);
1139
+ if (exited) return true;
1140
+ continue;
1141
+ }
1142
+ const vars = await this.gatherVariables(sub);
1143
+ const tokens = buildTokens(sub, vars);
1144
+ try {
1145
+ await sub._execute(tokens);
1146
+ } catch (err) {
1147
+ process.stderr.write(`${err instanceof Error ? err.message : err}
1148
+ `);
1149
+ }
1150
+ return false;
1151
+ }
1152
+ }
1153
+ async promptDrill(cmd, breadcrumb) {
1154
+ const subs = cmd.commandsArray.map((c) => c.name ?? "").filter(Boolean);
1155
+ const label = this.colorize(breadcrumb.join(" "));
1156
+ const choices = [...subs, "help"].join(", ");
1157
+ process.stdout.write(`
1158
+ ${label} ${choices}
1159
+ `);
1160
+ return new Promise((resolve) => {
1161
+ let resolved = false;
1162
+ const done = (val) => {
1163
+ if (!resolved) {
1164
+ resolved = true;
1165
+ resolve(val);
1166
+ }
1167
+ };
1168
+ this.rl.question("> ", (answer) => done(answer.trim() || null));
1169
+ this.rl.once("close", () => done(null));
1170
+ });
1171
+ }
1172
+ async gatherVariables(cmd) {
1173
+ const result = {};
1174
+ if (!cmd.variables) return result;
1175
+ for (const v of cmd.variables) {
1176
+ const name = v.name ?? "value";
1177
+ const hint = v.hint ? ` ${v.hint}` : "";
1178
+ while (true) {
1179
+ const answer = await new Promise((resolve) => {
1180
+ let resolved = false;
1181
+ const done = (val) => {
1182
+ if (!resolved) {
1183
+ resolved = true;
1184
+ resolve(val);
1185
+ }
1186
+ };
1187
+ this.rl.question(` ${name}${hint}: `, (ans) => done(ans.trim() || null));
1188
+ this.rl.once("close", () => done(null));
1189
+ });
1190
+ const value = answer ?? v.default ?? null;
1191
+ if (!value && v.required) {
1192
+ process.stderr.write(` ${name} is required
1193
+ `);
1194
+ continue;
1195
+ }
1196
+ if (value) {
1197
+ if (v.type === "enum" && v.enum && !v.enum.includes(value)) {
1198
+ process.stderr.write(` Must be one of: ${v.enum.join(", ")}
1199
+ `);
1200
+ continue;
1201
+ }
1202
+ result[name] = value;
1203
+ }
1204
+ break;
1205
+ }
1206
+ }
1207
+ return result;
1208
+ }
1209
+ // ── Free mode ─────────────────────────────────────────────────────────
1210
+ async freeLoop() {
1211
+ const prompt = `${this.colorize(this.opts.prompt)} > `;
1212
+ this.rl.setPrompt(prompt);
1213
+ this.rl.prompt();
1214
+ for await (const line of this.rl) {
1215
+ const trimmed = line.trim();
1216
+ if (!trimmed) {
1217
+ this.rl.prompt();
1218
+ continue;
1219
+ }
1220
+ if (this.opts.exitCommands.includes(trimmed)) break;
1221
+ const tokens = tokenize(trimmed);
1222
+ try {
1223
+ await this.root._execute(tokens);
1224
+ } catch (err) {
1225
+ process.stderr.write(`${err instanceof Error ? err.message : err}
1226
+ `);
1227
+ }
1228
+ this.rl.prompt();
1229
+ }
1230
+ }
1231
+ // ── Helpers ───────────────────────────────────────────────────────────
1232
+ colorize(text) {
1233
+ const c = this.opts.promptColor;
1234
+ if (!c || !process.stdout.isTTY) return text;
1235
+ try {
1236
+ if (c.startsWith("#")) return import_cosmetic2.default.hex(c).encoder(text);
1237
+ if (/^\d+$/.test(c)) return import_cosmetic2.default.xterm(Number(c)).encoder(text);
1238
+ const style = import_cosmetic2.default[c];
1239
+ if (style && typeof style.encoder === "function") return style.encoder(text);
1240
+ } catch {
1241
+ }
1242
+ return text;
1040
1243
  }
1041
1244
  };
1245
+ function tokenize(line) {
1246
+ const tokens = [];
1247
+ let current = "";
1248
+ let inQuote = null;
1249
+ for (const ch of line) {
1250
+ if (inQuote) {
1251
+ if (ch === inQuote) {
1252
+ inQuote = null;
1253
+ } else {
1254
+ current += ch;
1255
+ }
1256
+ } else if (ch === '"' || ch === "'") {
1257
+ inQuote = ch;
1258
+ } else if (ch === " " || ch === " ") {
1259
+ if (current) {
1260
+ tokens.push(current);
1261
+ current = "";
1262
+ }
1263
+ } else {
1264
+ current += ch;
1265
+ }
1266
+ }
1267
+ if (current) tokens.push(current);
1268
+ return tokens;
1269
+ }
1270
+ function buildTokens(cmd, vars) {
1271
+ if (!cmd.variables) return [];
1272
+ return cmd.variables.map((v) => vars[v.name ?? ""]).filter((v) => v !== void 0 && v !== "");
1273
+ }
1274
+ function makeCompleter(root) {
1275
+ return (line) => {
1276
+ const tokens = tokenize(line);
1277
+ let cmd = root;
1278
+ for (const token of tokens.slice(0, -1)) {
1279
+ const sub = cmd.commandsArray.find((c) => c.name === token);
1280
+ if (!sub) break;
1281
+ cmd = sub;
1282
+ }
1283
+ const partial = tokens[tokens.length - 1] ?? "";
1284
+ const names = [...cmd.commandsArray.map((c) => c.name ?? "").filter(Boolean), "help"];
1285
+ const hits = names.filter((n) => n.startsWith(partial));
1286
+ return [hits.length ? hits : names, partial];
1287
+ };
1288
+ }
1042
1289
 
1043
1290
  // src/models/Bar.ts
1044
1291
  var _Bar = class _Bar {
@@ -1500,7 +1747,7 @@ __export(Chart_exports, {
1500
1747
  Sparkline: () => Sparkline,
1501
1748
  VerticalBar: () => VerticalBar
1502
1749
  });
1503
- var import_cosmetic2 = __toESM(require("cosmetic"));
1750
+ var import_cosmetic3 = __toESM(require("cosmetic"));
1504
1751
 
1505
1752
  // src/utils/padLeft.ts
1506
1753
  var padLeft = (string, padding) => {
@@ -1521,9 +1768,9 @@ function formatNum(n) {
1521
1768
  }
1522
1769
  function applyConfigColor(s) {
1523
1770
  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);
1771
+ if (typeof c === "number") return import_cosmetic3.default.xterm(c).encoder(s);
1772
+ if (c.startsWith("#")) return import_cosmetic3.default.hex(c).encoder(s);
1773
+ return import_cosmetic3.default[c].encoder(s);
1527
1774
  }
1528
1775
  function applyPadding(str, paddingX, paddingY) {
1529
1776
  const lines = str.split("\n");
@@ -1549,7 +1796,7 @@ var Bar2 = class {
1549
1796
  const cols = options.width ?? process.stdout.columns ?? 80;
1550
1797
  const available = cols - maxKeyLen - maxValueLen - 3;
1551
1798
  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);
1799
+ 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
1800
  for (const item of data) {
1554
1801
  if (!item) {
1555
1802
  this.string += "\n";
@@ -1559,7 +1806,7 @@ var Bar2 = class {
1559
1806
  const keyPart = (rawKey ? encodeKey(rawKey) : "") + " ".repeat(maxKeyLen - stringLength(rawKey));
1560
1807
  const barWidth = Math.max(1, Math.floor(item.value * scale));
1561
1808
  let bar = (item.character ?? " ").repeat(barWidth);
1562
- bar = item.style ? item.style(bar) : import_cosmetic2.default.background.white.encoder(bar);
1809
+ bar = item.style ? item.style(bar) : import_cosmetic3.default.background.white.encoder(bar);
1563
1810
  this.string += `${keyPart}|${bar} ${item.value}
1564
1811
  `;
1565
1812
  }
@@ -1608,7 +1855,7 @@ var VerticalBar = class {
1608
1855
  }
1609
1856
  this.string += "\u2500".repeat(data.length * colWidth) + "\n";
1610
1857
  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);
1858
+ 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
1859
  let labels = "";
1613
1860
  for (const item of data) {
1614
1861
  if (!item?.key) {
@@ -2069,7 +2316,6 @@ var MultiBar = class {
2069
2316
  };
2070
2317
 
2071
2318
  // src/models/MultiSelect.ts
2072
- var CLEAR_LINE2 = "\x1B[2K";
2073
2319
  var CURSOR_UP3 = (n) => `\x1B[${n}A`;
2074
2320
  var DIM2 = "\x1B[2m";
2075
2321
  var MultiSelect = class {
@@ -2108,7 +2354,7 @@ var MultiSelect = class {
2108
2354
  const checked = /* @__PURE__ */ new Set();
2109
2355
  let error = null;
2110
2356
  let lastDrawnLines = 0;
2111
- process.stdout.write(HIDE_CURSOR);
2357
+ process.stdout.write(HIDE_CURSOR + DISABLE_WRAP);
2112
2358
  const glyph = this.promptGlyph ? `${colorText(this.promptColor, this.promptGlyph)} ` : "";
2113
2359
  const indent = " ".repeat(this.promptGlyph ? stringLength(this.promptGlyph) + 1 : 0);
2114
2360
  process.stdout.write(`${glyph}${prompt}
@@ -2118,16 +2364,26 @@ var MultiSelect = class {
2118
2364
  const q = searchQuery.toLowerCase();
2119
2365
  return items.map((item, i) => ({ item, originalIndex: i })).filter(({ item }) => item.label.toLowerCase().includes(q) || (item.description?.toLowerCase().includes(q) ?? false));
2120
2366
  };
2367
+ const computeMaxHeight = () => {
2368
+ const termRows = process.stdout.rows ?? 24;
2369
+ const reserved = 1 + (this.searchEnabled ? 1 : 0) + 1 + 1;
2370
+ const fit = Math.max(1, termRows - reserved - 1);
2371
+ return this.maxHeight ? Math.min(this.maxHeight, fit) : fit;
2372
+ };
2121
2373
  const renderList = (redraw) => {
2122
2374
  const filtered = getFiltered();
2123
2375
  if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
2124
- if (this.maxHeight) {
2376
+ const maxHeight = computeMaxHeight();
2377
+ const useViewport = filtered.length > maxHeight;
2378
+ if (useViewport) {
2125
2379
  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)));
2380
+ else if (cursor >= viewportOffset + maxHeight) viewportOffset = cursor - maxHeight + 1;
2381
+ viewportOffset = Math.max(0, Math.min(viewportOffset, Math.max(0, filtered.length - maxHeight)));
2382
+ } else {
2383
+ viewportOffset = 0;
2128
2384
  }
2129
- const visibleStart = this.maxHeight ? viewportOffset : 0;
2130
- const visibleEnd = this.maxHeight ? Math.min(filtered.length, viewportOffset + this.maxHeight) : filtered.length;
2385
+ const visibleStart = useViewport ? viewportOffset : 0;
2386
+ const visibleEnd = useViewport ? Math.min(filtered.length, viewportOffset + maxHeight) : filtered.length;
2131
2387
  if (redraw) {
2132
2388
  if (lastDrawnLines > 0) process.stdout.write(CURSOR_UP3(lastDrawnLines));
2133
2389
  process.stdout.write("\r\x1B[0J");
@@ -2143,14 +2399,15 @@ var MultiSelect = class {
2143
2399
  const { item, originalIndex } = filtered[vi];
2144
2400
  const isCursor = vi === cursor;
2145
2401
  const isChecked = checked.has(originalIndex);
2402
+ const numStr = `${vi + 1}.`;
2146
2403
  const desc = item.description ? ` ${colorText(this.descriptionColor, `\u2014 ${item.description}`)}` : "";
2147
2404
  const checkMark = isChecked ? pulse ? `${pulse}${this.checkedPrefix}${RESET}` : colorText(this.promptColor, this.checkedPrefix) : this.uncheckedPrefix;
2148
2405
  const label = isCursor ? pulse ? `${pulse}${item.label}${RESET}` : colorText(this.promptColor, item.label) : item.label;
2149
- process.stdout.write(`\r${indent}${checkMark} ${label}${desc}
2406
+ process.stdout.write(`\r${indent}${numStr} ${checkMark} ${label}${desc}
2150
2407
  `);
2151
2408
  lastDrawnLines++;
2152
2409
  }
2153
- const hintContent = `\u2191\u2193 move space/tab toggle \u2190\u2192 deselect/select${this.searchEnabled ? " type to filter" : " a all"} enter confirm`;
2410
+ 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
2411
  const maxHintCols = Math.max(10, (process.stdout.columns ?? 80) - stringLength(indent) - 1);
2155
2412
  const hint = stringLength(hintContent) > maxHintCols ? hintContent.slice(0, maxHintCols) : hintContent;
2156
2413
  process.stdout.write(`\r${indent}${DIM2}${hint}${RESET}
@@ -2173,7 +2430,7 @@ var MultiSelect = class {
2173
2430
  process.stdin.setRawMode(false);
2174
2431
  process.stdin.pause();
2175
2432
  process.stdin.removeListener("data", onKey);
2176
- process.stdout.write(SHOW_CURSOR);
2433
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
2177
2434
  });
2178
2435
  const cleanup = (result) => {
2179
2436
  deregisterCleanup();
@@ -2181,16 +2438,13 @@ var MultiSelect = class {
2181
2438
  clearInterval(timer);
2182
2439
  timer = null;
2183
2440
  }
2184
- if (lastDrawnLines > 0) process.stdout.write(CURSOR_UP3(lastDrawnLines));
2185
- process.stdout.write("\x1B[0J");
2186
- const bulletWidth = stringLength(this.checkedPrefix) + 1;
2187
- for (let i = 0; i < items.length; i++) {
2188
- const item = items[i];
2189
- const bullet = checked.has(i) ? `${colorText(this.promptColor, this.checkedPrefix)} ` : " ".repeat(bulletWidth);
2190
- process.stdout.write(`\r${indent}${bullet}${item.label}
2441
+ process.stdout.write(CURSOR_UP3(lastDrawnLines + 1));
2442
+ process.stdout.write("\r\x1B[0J");
2443
+ process.stdout.write(ENABLE_WRAP);
2444
+ const selectedItems = items.filter((_, i) => checked.has(i));
2445
+ const display = result !== null && selectedItems.length > 0 ? selectedItems.map((i) => i.label).join(", ") : "\u2014";
2446
+ process.stdout.write(`${glyph}${prompt}: ${display}
2191
2447
  `);
2192
- }
2193
- process.stdout.write(`\r${CLEAR_LINE2}`);
2194
2448
  process.stdin.setRawMode(false);
2195
2449
  process.stdin.pause();
2196
2450
  process.stdin.removeListener("data", onKey);
@@ -2283,7 +2537,7 @@ var MultiSelect = class {
2283
2537
  process.stdin.setRawMode(false);
2284
2538
  process.stdin.pause();
2285
2539
  process.stdin.removeListener("data", onKey);
2286
- process.stdout.write(SHOW_CURSOR);
2540
+ process.stdout.write(ENABLE_WRAP + SHOW_CURSOR);
2287
2541
  process.exit(130);
2288
2542
  } else if (this.searchEnabled) {
2289
2543
  if (str === "\x7F" || str === "\b") {
@@ -2303,7 +2557,7 @@ var MultiSelect = class {
2303
2557
  const n = parseInt(str);
2304
2558
  if (!isNaN(n) && n >= 1 && n <= Math.min(filtered.length, 9)) {
2305
2559
  error = null;
2306
- cursor = viewportOffset + n - 1;
2560
+ cursor = n - 1;
2307
2561
  renderList(true);
2308
2562
  }
2309
2563
  }
@@ -2677,7 +2931,7 @@ _Spinner.FRAMES = {
2677
2931
  var Spinner = _Spinner;
2678
2932
 
2679
2933
  // src/models/Table.ts
2680
- var import_cosmetic3 = __toESM(require("cosmetic"));
2934
+ var import_cosmetic4 = __toESM(require("cosmetic"));
2681
2935
 
2682
2936
  // src/utils/padSides.ts
2683
2937
  var padSides = (string, padding) => {
@@ -2755,7 +3009,7 @@ var Table = class {
2755
3009
  header += pad(column.title, column.padding + this.margin);
2756
3010
  if (i < keys.length - 1) header += this.separator;
2757
3011
  }
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];
3012
+ 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
3013
  this.string += `${styled.underline.encoder(header)}
2760
3014
  `;
2761
3015
  for (const [ri, row] of this.rows.entries()) {
@@ -2826,13 +3080,13 @@ _TermKit.commandDefaults = {};
2826
3080
  var TermKit = _TermKit;
2827
3081
 
2828
3082
  // src/index.ts
2829
- var import_cosmetic4 = __toESM(require("cosmetic"));
3083
+ var import_cosmetic5 = __toESM(require("cosmetic"));
2830
3084
  var base = null;
2831
3085
  var commandDefaults = {};
2832
3086
  var Program = {
2833
3087
  command: (name, variables, info) => {
2834
3088
  const cmd = new Command(Object.assign({ name, variables, info }, commandDefaults));
2835
- if (!base) base = cmd;
3089
+ base = cmd;
2836
3090
  return cmd;
2837
3091
  },
2838
3092
  option: (short, long, variables, info) => new Option({ short, long, variables, info }),
@@ -2855,6 +3109,10 @@ var Program = {
2855
3109
  },
2856
3110
  setDefaults: (data) => {
2857
3111
  commandDefaults = data;
3112
+ },
3113
+ shell: async (options) => {
3114
+ if (!base) throw new Error("No command defined");
3115
+ return new Shell(base, options).run();
2858
3116
  }
2859
3117
  };
2860
3118
  // Annotate the CommonJS export names for ESM import in node:
@@ -2872,6 +3130,7 @@ var Program = {
2872
3130
  Program,
2873
3131
  Scrollbox,
2874
3132
  Select,
3133
+ Shell,
2875
3134
  Spinner,
2876
3135
  Table,
2877
3136
  TermKit,