@nmakarov/cli-toolkit 0.7.0 → 0.7.2

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
@@ -1,9 +1,1101 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
1
3
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
4
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
5
  }) : x)(function(x) {
4
6
  if (typeof require !== "undefined") return require.apply(this, arguments);
5
7
  throw Error('Dynamic require of "' + x + '" is not supported');
6
8
  });
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/screen/components.ts
18
+ import { createElement as h } from "react";
19
+ import { Box, Text } from "ink";
20
+ function getScreenWidth(maxWidth = null) {
21
+ const terminalWidth = process.stdout.columns || 80;
22
+ const availableWidth = Math.max(20, terminalWidth - 4);
23
+ return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
24
+ }
25
+ function ScreenContainer({ children }) {
26
+ const width = getScreenWidth();
27
+ return h(Box, {
28
+ flexDirection: "column",
29
+ marginTop: 1,
30
+ borderStyle: "single",
31
+ borderColor: "cyan",
32
+ paddingX: 1,
33
+ width
34
+ // Use the calculated width directly
35
+ }, children);
36
+ }
37
+ function ScreenRow({ children }) {
38
+ return h(Box, { flexDirection: "column" }, children);
39
+ }
40
+ function ScreenTitle({ text }) {
41
+ return h(
42
+ ScreenRow,
43
+ {},
44
+ h(Text, { bold: true, color: "cyan" }, text)
45
+ );
46
+ }
47
+ function ScreenDivider({ width }) {
48
+ const dividerWidth = width || getScreenWidth() - 4;
49
+ return h(Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
50
+ }
51
+ function ScreenBody({ children, alignItems = "flex-start" }) {
52
+ return h(Box, { flexDirection: "column", alignItems }, children);
53
+ }
54
+ function ScreenFooter({ lines, textStyle }) {
55
+ const defaultTextStyle = {
56
+ dimColor: true,
57
+ color: "white"
58
+ };
59
+ const finalTextStyle = { ...defaultTextStyle, ...textStyle };
60
+ const flattenAndWrap = (items, keyPrefix = "") => {
61
+ const result = [];
62
+ let keyIndex = 0;
63
+ items.forEach((item, index) => {
64
+ if (Array.isArray(item)) {
65
+ const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
66
+ result.push(...nested);
67
+ } else if (typeof item === "string") {
68
+ result.push(
69
+ h(Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
70
+ );
71
+ } else {
72
+ const element = item;
73
+ if (element.key === null || element.key === void 0) {
74
+ result.push(
75
+ h(Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
76
+ );
77
+ } else {
78
+ result.push(element);
79
+ }
80
+ }
81
+ });
82
+ return result;
83
+ };
84
+ const wrappedItems = flattenAndWrap(lines);
85
+ return h(
86
+ Box,
87
+ { flexDirection: "column" },
88
+ h(Box, { flexDirection: "row" }, ...wrappedItems)
89
+ );
90
+ }
91
+ var init_components = __esm({
92
+ "src/screen/components.ts"() {
93
+ "use strict";
94
+ }
95
+ });
96
+
97
+ // src/screen/list-components.ts
98
+ import React2, { useState, useEffect, useRef, createElement } from "react";
99
+ import { Box as Box2, Text as Text2 } from "ink";
100
+ function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
101
+ const [, forceUpdate] = useState({});
102
+ const termWidth = (process.stdout.columns || 80) - 8;
103
+ const maxItemLength = Math.max(...items.map((w) => w.length));
104
+ const columnWidth = maxItemLength + 3;
105
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
106
+ const itemsPerColumn = Math.ceil(items.length / columns);
107
+ useEffect(() => {
108
+ ctx.setAction("moveUp", () => {
109
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
110
+ forceUpdate({});
111
+ });
112
+ ctx.setAction("moveDown", () => {
113
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
114
+ forceUpdate({});
115
+ });
116
+ ctx.setAction("moveLeft", () => {
117
+ if (selectedIndexRef.current === 0) {
118
+ ctx.goBack();
119
+ } else {
120
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
121
+ forceUpdate({});
122
+ }
123
+ });
124
+ ctx.setAction("moveRight", () => {
125
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
126
+ forceUpdate({});
127
+ });
128
+ ctx.setKeyBinding([
129
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
130
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
131
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
132
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
133
+ ]);
134
+ ctx.addFooter(`Total: ${items.length} items`);
135
+ }, []);
136
+ const selectedIndex = selectedIndexRef.current;
137
+ const rows = [];
138
+ for (let row = 0; row < itemsPerColumn; row++) {
139
+ const cols = [];
140
+ for (let col = 0; col < columns; col++) {
141
+ const index = col * itemsPerColumn + row;
142
+ if (index < items.length) {
143
+ const isSelected = index === selectedIndex;
144
+ cols.push(
145
+ h2(
146
+ Box2,
147
+ { key: index, width: columnWidth },
148
+ h2(Text2, {
149
+ color: isSelected ? "black" : "white",
150
+ backgroundColor: isSelected ? "cyan" : void 0,
151
+ bold: isSelected
152
+ }, items[index].padEnd(maxItemLength))
153
+ )
154
+ );
155
+ }
156
+ }
157
+ rows.push(
158
+ h2(ScreenRow, { key: row, children: h2(Box2, { flexDirection: "row" }, ...cols) })
159
+ );
160
+ }
161
+ return h2(Box2, { flexDirection: "column" }, ...rows);
162
+ }
163
+ function MultiColumnListWithPreviewComponent({
164
+ items,
165
+ getPreviewContent,
166
+ ctx,
167
+ selectedIndexRef
168
+ }) {
169
+ const [, forceUpdate] = useState({});
170
+ const termWidth = (process.stdout.columns || 80) - 8;
171
+ const maxItemLength = Math.max(...items.map((w) => w.length));
172
+ const columnWidth = maxItemLength + 3;
173
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
174
+ const itemsPerColumn = Math.ceil(items.length / columns);
175
+ useEffect(() => {
176
+ ctx.setAction("moveUp", () => {
177
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
178
+ forceUpdate({});
179
+ });
180
+ ctx.setAction("moveDown", () => {
181
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
182
+ forceUpdate({});
183
+ });
184
+ ctx.setAction("moveLeft", () => {
185
+ if (selectedIndexRef.current === 0) {
186
+ ctx.goBack();
187
+ } else {
188
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
189
+ forceUpdate({});
190
+ }
191
+ });
192
+ ctx.setAction("moveRight", () => {
193
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
194
+ forceUpdate({});
195
+ });
196
+ ctx.setKeyBinding([
197
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
198
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
199
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
200
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
201
+ ]);
202
+ ctx.addFooter(`Total: ${items.length} items`);
203
+ }, []);
204
+ const selectedIndex = selectedIndexRef.current;
205
+ const selectedItem = items[selectedIndex];
206
+ const rows = [];
207
+ for (let row = 0; row < itemsPerColumn; row++) {
208
+ const cols = [];
209
+ for (let col = 0; col < columns; col++) {
210
+ const index = col * itemsPerColumn + row;
211
+ if (index < items.length) {
212
+ const isSelected = index === selectedIndex;
213
+ cols.push(
214
+ h2(
215
+ Box2,
216
+ { key: index, width: columnWidth },
217
+ h2(Text2, {
218
+ color: isSelected ? "black" : "white",
219
+ backgroundColor: isSelected ? "cyan" : void 0,
220
+ bold: isSelected
221
+ }, items[index].padEnd(maxItemLength))
222
+ )
223
+ );
224
+ }
225
+ }
226
+ rows.push(
227
+ h2(ScreenRow, { key: row, children: h2(Box2, { flexDirection: "row" }, ...cols) })
228
+ );
229
+ }
230
+ const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
231
+ const previewRows = [];
232
+ if (typeof previewContent === "string") {
233
+ previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
234
+ } else if (typeof previewContent === "object" && !React2.isValidElement(previewContent) && previewContent !== null) {
235
+ Object.entries(previewContent).forEach(([key, value], idx) => {
236
+ previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
237
+ });
238
+ } else if (React2.isValidElement(previewContent)) {
239
+ previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
240
+ }
241
+ return h2(
242
+ Box2,
243
+ { flexDirection: "column" },
244
+ ...rows,
245
+ h2(ScreenRow, { key: "spacer-1", children: h2(Text2, {}, " ") }),
246
+ h2(ScreenDivider, { key: "divider" }),
247
+ h2(ScreenRow, { key: "spacer-2", children: h2(Text2, {}, " ") }),
248
+ ...previewRows
249
+ );
250
+ }
251
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
252
+ const [, forceUpdate] = useState({});
253
+ const [sortOrder, setSortOrder] = useState("none");
254
+ const [scrollOffset, setScrollOffset] = useState(0);
255
+ const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
256
+ const defaultGetTitle = (item) => {
257
+ return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
258
+ };
259
+ const titleGetter = getTitle || defaultGetTitle;
260
+ const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
261
+ const titleA = titleGetter(a).toLowerCase();
262
+ const titleB = titleGetter(b).toLowerCase();
263
+ if (sortOrder === "asc") {
264
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
265
+ } else {
266
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
267
+ }
268
+ }) : items;
269
+ const effectiveMaxHeight = maxHeight || displayItems.length;
270
+ const canScroll = displayItems.length > effectiveMaxHeight;
271
+ const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
272
+ const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
273
+ const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
274
+ const canScrollUp = clampedScrollOffset > 0;
275
+ const canScrollDown = clampedScrollOffset < maxScrollOffset;
276
+ scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
277
+ useEffect(() => {
278
+ ctx.setAction("moveUp", () => {
279
+ const newIndex = Math.max(0, selectedIndexRef.current - 1);
280
+ selectedIndexRef.current = newIndex;
281
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
282
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
283
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
284
+ if (newIndex < currentClampedScrollOffset) {
285
+ setScrollOffset(newIndex);
286
+ }
287
+ forceUpdate({});
288
+ });
289
+ ctx.setAction("moveDown", () => {
290
+ const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
291
+ const titleA = titleGetter(a).toLowerCase();
292
+ const titleB = titleGetter(b).toLowerCase();
293
+ if (sortOrder === "asc") {
294
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
295
+ } else {
296
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
297
+ }
298
+ }) : items;
299
+ const maxIndex = currentItems.length - 1;
300
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
301
+ selectedIndexRef.current = newIndex;
302
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
303
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
304
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
305
+ if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
306
+ setScrollOffset(newIndex - currentMaxHeight + 1);
307
+ }
308
+ forceUpdate({});
309
+ });
310
+ ctx.setAction("scrollUp", () => {
311
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
312
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
313
+ const newScrollOffset = Math.max(0, currentScrollOffset - 1);
314
+ setScrollOffset(newScrollOffset);
315
+ forceUpdate({});
316
+ });
317
+ ctx.setAction("scrollDown", () => {
318
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
319
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
320
+ const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
321
+ setScrollOffset(newScrollOffset);
322
+ forceUpdate({});
323
+ });
324
+ if (sortable) {
325
+ ctx.setAction("toggleSort", () => {
326
+ const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
327
+ const currentSelectedItem = displayItems[selectedIndexRef.current];
328
+ setSortOrder(nextSort);
329
+ const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
330
+ const titleA = titleGetter(a).toLowerCase();
331
+ const titleB = titleGetter(b).toLowerCase();
332
+ if (nextSort === "asc") {
333
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
334
+ } else {
335
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
336
+ }
337
+ }) : items;
338
+ const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
339
+ if (newIndex !== -1) {
340
+ selectedIndexRef.current = newIndex;
341
+ setScrollOffset(newIndex);
342
+ } else {
343
+ selectedIndexRef.current = 0;
344
+ setScrollOffset(0);
345
+ }
346
+ forceUpdate({});
347
+ });
348
+ const defaultHighlightStyle = {
349
+ color: "black",
350
+ backgroundColor: "green",
351
+ bold: true
352
+ };
353
+ const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
354
+ const sortCaption = () => {
355
+ if (sortOrder === "none") {
356
+ return h2(Text2, {}, "s to toggle sort");
357
+ } else {
358
+ const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
359
+ return h2(
360
+ Text2,
361
+ {},
362
+ "s to toggle ",
363
+ h2(Text2, { color: "white", bold: true }, "sort"),
364
+ " ",
365
+ h2(Text2, highlightStyle, ` ${sortLabel} `)
366
+ );
367
+ }
368
+ };
369
+ ctx.setKeyBinding([
370
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
371
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
372
+ {
373
+ key: "s",
374
+ caption: sortCaption,
375
+ action: "toggleSort",
376
+ order: 5
377
+ }
378
+ ]);
379
+ ctx.update();
380
+ } else {
381
+ ctx.setKeyBinding([
382
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
383
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
384
+ ]);
385
+ }
386
+ }, [sortOrder, sortable]);
387
+ const selectedIndex = selectedIndexRef.current;
388
+ const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
389
+ const isFirstVisible = displayIndex === 0;
390
+ const isLastVisible = displayIndex === visibleItems.length - 1;
391
+ let arrowPrefix = "";
392
+ let selectionPrefix = "";
393
+ if (isFirstVisible && canScrollUp) {
394
+ arrowPrefix = "\u2191 ";
395
+ } else if (isLastVisible && canScrollDown) {
396
+ arrowPrefix = "\u2193 ";
397
+ } else {
398
+ arrowPrefix = " ";
399
+ }
400
+ if (isSelected) {
401
+ selectionPrefix = selectionMarker;
402
+ } else {
403
+ selectionPrefix = " ".repeat(selectionMarker.length);
404
+ }
405
+ return h2(
406
+ Box2,
407
+ { flexDirection: "row" },
408
+ // Arrow (clickable if functional, not highlighted)
409
+ h2(Text2, {
410
+ key: `arrow-${actualIndex}`,
411
+ color: "white"
412
+ }, arrowPrefix),
413
+ // Selection marker space (always same width, not highlighted)
414
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
415
+ // Item name (highlighted if selected)
416
+ h2(Text2, {
417
+ key: `name-${actualIndex}`,
418
+ color: isSelected ? "black" : "white",
419
+ backgroundColor: isSelected ? "cyan" : void 0,
420
+ bold: isSelected
421
+ }, item.name)
422
+ );
423
+ };
424
+ const itemRenderer = renderItem || defaultRenderItem;
425
+ return h2(
426
+ Box2,
427
+ { flexDirection: "column" },
428
+ ...visibleItems.map((item, displayIndex) => {
429
+ const actualIndex = clampedScrollOffset + displayIndex;
430
+ const isSelected = actualIndex === selectedIndex;
431
+ if (renderItem) {
432
+ const isFirstVisible = displayIndex === 0;
433
+ const isLastVisible = displayIndex === visibleItems.length - 1;
434
+ let arrowPrefix = "";
435
+ let selectionPrefix = "";
436
+ if (isFirstVisible && canScrollUp) {
437
+ arrowPrefix = "\u2191 ";
438
+ } else if (isLastVisible && canScrollDown) {
439
+ arrowPrefix = "\u2193 ";
440
+ } else {
441
+ arrowPrefix = " ";
442
+ }
443
+ if (isSelected) {
444
+ selectionPrefix = selectionMarker;
445
+ } else {
446
+ selectionPrefix = " ".repeat(selectionMarker.length);
447
+ }
448
+ return h2(ScreenRow, {
449
+ key: `item-${actualIndex}`,
450
+ children: h2(
451
+ Box2,
452
+ { flexDirection: "row" },
453
+ // Arrow (clickable if functional, not highlighted)
454
+ h2(Text2, {
455
+ key: `arrow-${actualIndex}`,
456
+ color: "white"
457
+ }, arrowPrefix),
458
+ // Selection marker space (always same width, not highlighted)
459
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
460
+ // Custom rendered content
461
+ renderItem(item, isSelected, displayIndex)
462
+ )
463
+ });
464
+ } else {
465
+ return h2(ScreenRow, {
466
+ key: `item-${actualIndex}`,
467
+ children: itemRenderer(item, isSelected, displayIndex, actualIndex)
468
+ });
469
+ }
470
+ })
471
+ );
472
+ }
473
+ var h2;
474
+ var init_list_components = __esm({
475
+ "src/screen/list-components.ts"() {
476
+ "use strict";
477
+ init_components();
478
+ h2 = createElement;
479
+ }
480
+ });
481
+
482
+ // src/screen/screens.ts
483
+ import { useState as useState2, createElement as h3 } from "react";
484
+ import { render, useInput, Text as Text3 } from "ink";
485
+ function groupKeyBindings(bindings) {
486
+ const groups = {};
487
+ const enabledBindings = bindings.filter((b) => b.enabled !== false);
488
+ enabledBindings.forEach((binding) => {
489
+ const caption = typeof binding.caption === "string" ? binding.caption : "";
490
+ if (!groups[caption]) {
491
+ groups[caption] = {
492
+ keys: [],
493
+ caption,
494
+ order: binding.order || 999
495
+ };
496
+ }
497
+ groups[caption].keys.push(binding.key);
498
+ });
499
+ return Object.values(groups);
500
+ }
501
+ function formatKeyBindings(bindings, mode = "long") {
502
+ const resolvedBindings = bindings.map((binding) => {
503
+ let resolvedCaption = binding.caption;
504
+ if (typeof binding.caption === "function") {
505
+ resolvedCaption = binding.caption();
506
+ }
507
+ return {
508
+ ...binding,
509
+ resolvedCaption
510
+ };
511
+ });
512
+ const groups = groupKeyBindings(resolvedBindings.map((b) => ({
513
+ ...b,
514
+ caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
515
+ })));
516
+ groups.sort((a, b) => a.order - b.order);
517
+ const items = [];
518
+ groups.forEach((group) => {
519
+ const bindingWithCustom = resolvedBindings.find(
520
+ (b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
521
+ );
522
+ if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
523
+ items.push(bindingWithCustom.resolvedCaption);
524
+ } else {
525
+ const keyStr = formatKeys(group.keys);
526
+ if (mode === "long") {
527
+ items.push(`${keyStr} to ${group.caption}`);
528
+ } else {
529
+ items.push(keyStr);
530
+ }
531
+ }
532
+ });
533
+ return items;
534
+ }
535
+ function formatKeys(keys) {
536
+ const keyMap = {
537
+ "escape": "esc",
538
+ "leftArrow": "\u2190",
539
+ "rightArrow": "\u2192",
540
+ "upArrow": "\u2191",
541
+ "downArrow": "\u2193",
542
+ "return": "enter"
543
+ };
544
+ return keys.map((k) => keyMap[k] || k).join("/");
545
+ }
546
+ async function showScreen(config2) {
547
+ const {
548
+ title,
549
+ onRender,
550
+ parentData = {}
551
+ } = config2;
552
+ return new Promise((resolve2) => {
553
+ let instance;
554
+ const keyBindings = [];
555
+ const actions = {};
556
+ const customFooterItems = [];
557
+ let renderResult = null;
558
+ let initialized = false;
559
+ const Screen = () => {
560
+ const [updateCounter, setUpdateCounter] = useState2(0);
561
+ if (!initialized) {
562
+ const defaultBindings = [
563
+ { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
564
+ { key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
565
+ // Note: 'select' is not a default - components add it if needed
566
+ ];
567
+ defaultBindings.forEach((binding) => {
568
+ keyBindings.push(binding);
569
+ });
570
+ actions.back = () => {
571
+ cleanup(null);
572
+ };
573
+ initialized = true;
574
+ }
575
+ const context = {
576
+ setAction: (actionName, handlerFn) => {
577
+ actions[actionName] = handlerFn;
578
+ },
579
+ setKeyBinding: (bindingOrBindings) => {
580
+ const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
581
+ bindingsToSet.forEach((binding) => {
582
+ const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
583
+ if (existingIndex >= 0) {
584
+ const existing = keyBindings[existingIndex];
585
+ if (existing.protected) {
586
+ console.warn(`Cannot override protected key: ${binding.key}`);
587
+ return;
588
+ }
589
+ keyBindings[existingIndex] = {
590
+ ...existing,
591
+ ...binding,
592
+ order: binding.order !== void 0 ? binding.order : existing.order,
593
+ enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
594
+ };
595
+ } else {
596
+ keyBindings.push({
597
+ protected: false,
598
+ order: 999,
599
+ enabled: true,
600
+ ...binding
601
+ });
602
+ }
603
+ });
604
+ },
605
+ updateKeyBinding: (keyName, updates) => {
606
+ const index = keyBindings.findIndex((b) => b.key === keyName);
607
+ if (index >= 0) {
608
+ keyBindings[index] = {
609
+ ...keyBindings[index],
610
+ ...updates
611
+ };
612
+ }
613
+ },
614
+ removeKeyBinding: (keyName) => {
615
+ const index = keyBindings.findIndex((b) => b.key === keyName);
616
+ if (index >= 0) {
617
+ if (keyBindings[index].protected) {
618
+ console.warn(`Cannot remove protected key: ${keyName}`);
619
+ return;
620
+ }
621
+ keyBindings.splice(index, 1);
622
+ }
623
+ },
624
+ addFooter: (item) => {
625
+ customFooterItems.push(item);
626
+ },
627
+ clearFooter: () => {
628
+ customFooterItems.length = 0;
629
+ },
630
+ setFooter: (items) => {
631
+ customFooterItems.length = 0;
632
+ const itemsArray = Array.isArray(items) ? items : [items];
633
+ customFooterItems.push(...itemsArray);
634
+ },
635
+ update: () => {
636
+ setUpdateCounter((c) => c + 1);
637
+ },
638
+ goBack: () => {
639
+ if (actions.back) {
640
+ actions.back();
641
+ }
642
+ },
643
+ close: (result) => {
644
+ cleanup(result);
645
+ },
646
+ parentData
647
+ };
648
+ if (!renderResult) {
649
+ renderResult = onRender(context);
650
+ }
651
+ useInput((input, key) => {
652
+ if (key.ctrl && input === "c") {
653
+ cleanup(null);
654
+ process.exit(0);
655
+ return;
656
+ }
657
+ let matchedBinding = null;
658
+ for (const binding of keyBindings) {
659
+ let keyMatches = false;
660
+ if (key[binding.key]) {
661
+ keyMatches = true;
662
+ } else if (input === 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
+ }
675
+ }
676
+ if (matchedBinding && actions[matchedBinding.action]) {
677
+ const actionResult = actions[matchedBinding.action]({
678
+ input,
679
+ key,
680
+ binding: matchedBinding
681
+ });
682
+ }
683
+ });
684
+ const footerLines = [];
685
+ const bindingItems = formatKeyBindings(keyBindings, "long");
686
+ if (bindingItems.length > 0) {
687
+ const bindingsLine = [];
688
+ bindingItems.forEach((item, idx) => {
689
+ if (idx > 0) {
690
+ bindingsLine.push(", ");
691
+ }
692
+ bindingsLine.push(item);
693
+ });
694
+ const allStrings = bindingItems.every((item) => typeof item === "string");
695
+ if (allStrings) {
696
+ footerLines.push(bindingsLine.join(""));
697
+ } else {
698
+ const wrappedBindingsLine = bindingsLine.map(
699
+ (item) => typeof item === "string" ? h3(Text3, {}, item) : item
700
+ );
701
+ footerLines.push(wrappedBindingsLine);
702
+ }
703
+ }
704
+ customFooterItems.forEach((item) => {
705
+ if (typeof item === "string") {
706
+ footerLines.push(item);
707
+ } else {
708
+ footerLines.push(item);
709
+ }
710
+ });
711
+ return h3(
712
+ ScreenContainer,
713
+ {},
714
+ h3(ScreenTitle, { text: title }),
715
+ h3(ScreenDivider),
716
+ h3(ScreenRow, {}, h3(Text3, {}, " ")),
717
+ renderResult,
718
+ h3(ScreenRow, {}, h3(Text3, {}, " ")),
719
+ h3(ScreenDivider),
720
+ h3(ScreenFooter, { lines: footerLines })
721
+ );
722
+ };
723
+ const cleanup = (result) => {
724
+ if (instance) instance.unmount();
725
+ setTimeout(() => resolve2(result), 50);
726
+ };
727
+ instance = render(h3(Screen));
728
+ });
729
+ }
730
+ async function showListScreen(config2) {
731
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
732
+ return showScreen({
733
+ title,
734
+ parentData,
735
+ onRender: (ctx) => {
736
+ const selectedIndexRef = { current: initialSelectedIndex };
737
+ ctx.setAction("select", () => {
738
+ const selected = items[selectedIndexRef.current];
739
+ if (onSelect) {
740
+ const result = onSelect(selected.value, selectedIndexRef.current);
741
+ ctx.close(result);
742
+ }
743
+ });
744
+ if (onEscape) {
745
+ ctx.setAction("back", () => {
746
+ const result = onEscape(selectedIndexRef.current);
747
+ ctx.close(result);
748
+ });
749
+ }
750
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
751
+ return h3(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
752
+ }
753
+ });
754
+ }
755
+ async function showMultiColumnListScreen(config2) {
756
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
757
+ return showScreen({
758
+ title,
759
+ parentData,
760
+ onRender: (ctx) => {
761
+ const selectedIndexRef = { current: initialSelectedIndex };
762
+ ctx.setAction("select", () => {
763
+ const selected = items[selectedIndexRef.current];
764
+ if (onSelect) {
765
+ const result = onSelect(selected, selectedIndexRef.current);
766
+ ctx.close(result);
767
+ }
768
+ });
769
+ if (onEscape) {
770
+ ctx.setAction("back", () => {
771
+ const result = onEscape(selectedIndexRef.current);
772
+ ctx.close(result);
773
+ });
774
+ }
775
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
776
+ return h3(MultiColumnListComponent, { items, ctx, selectedIndexRef });
777
+ }
778
+ });
779
+ }
780
+ async function showMultiColumnListWithPreviewScreen(config2) {
781
+ const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
782
+ return showScreen({
783
+ title,
784
+ parentData,
785
+ onRender: (ctx) => {
786
+ const selectedIndexRef = { current: initialSelectedIndex };
787
+ ctx.setAction("select", () => {
788
+ const selected = items[selectedIndexRef.current];
789
+ if (onSelect) {
790
+ const result = onSelect(selected, selectedIndexRef.current);
791
+ ctx.close(result);
792
+ }
793
+ });
794
+ if (onEscape) {
795
+ ctx.setAction("back", () => {
796
+ const result = onEscape(selectedIndexRef.current);
797
+ ctx.close(result);
798
+ });
799
+ }
800
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
801
+ return h3(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
802
+ }
803
+ });
804
+ }
805
+ var showMenuScreen, showWordGridScreen;
806
+ var init_screens = __esm({
807
+ "src/screen/screens.ts"() {
808
+ "use strict";
809
+ init_components();
810
+ init_list_components();
811
+ showMenuScreen = showListScreen;
812
+ showWordGridScreen = showMultiColumnListScreen;
813
+ }
814
+ });
815
+
816
+ // src/screen/ui-elements.ts
817
+ import { createElement as h4 } from "react";
818
+ import { Box as Box4, Text as Text4 } from "ink";
819
+ function ListItem({
820
+ children,
821
+ isSelected = false,
822
+ color = "white",
823
+ backgroundColor,
824
+ bold = false,
825
+ dimColor = false
826
+ }) {
827
+ return h4(
828
+ Box4,
829
+ {},
830
+ h4(Text4, {
831
+ color: isSelected ? backgroundColor || "green" : color,
832
+ backgroundColor: isSelected ? color : backgroundColor,
833
+ bold: isSelected || bold,
834
+ dimColor: !isSelected && dimColor
835
+ }, children)
836
+ );
837
+ }
838
+ function TextBlock({
839
+ text,
840
+ color = "white",
841
+ dimmed = false,
842
+ bold = false,
843
+ maxWidth
844
+ }) {
845
+ return h4(
846
+ Box4,
847
+ {},
848
+ h4(Text4, {
849
+ color,
850
+ dimColor: dimmed,
851
+ bold
852
+ }, text)
853
+ );
854
+ }
855
+ function Divider({ character = "\u2500", width = 80 }) {
856
+ return h4(
857
+ Box4,
858
+ { marginY: 1 },
859
+ h4(Text4, { dimColor: true }, character.repeat(width))
860
+ );
861
+ }
862
+ function GridCell({
863
+ children,
864
+ width,
865
+ color = "white",
866
+ backgroundColor,
867
+ bold = false,
868
+ dimColor = false,
869
+ align = "left"
870
+ }) {
871
+ return h4(
872
+ Box4,
873
+ { width },
874
+ h4(Text4, {
875
+ color,
876
+ backgroundColor,
877
+ bold,
878
+ dimColor,
879
+ textAlign: align
880
+ }, children)
881
+ );
882
+ }
883
+ function InputField({ prompt, value, onChange, onSubmit }) {
884
+ return h4(
885
+ Box4,
886
+ { flexDirection: "column" },
887
+ h4(Text4, {}, prompt),
888
+ h4(
889
+ Box4,
890
+ { marginTop: 1 },
891
+ h4(Text4, { color: "cyan" }, " > ", value, "_")
892
+ )
893
+ );
894
+ }
895
+ var init_ui_elements = __esm({
896
+ "src/screen/ui-elements.ts"() {
897
+ "use strict";
898
+ }
899
+ });
900
+
901
+ // src/screen/utils.ts
902
+ function buildBreadcrumb(parts) {
903
+ if (parts.length === 0) return "";
904
+ if (parts.length === 1) return parts[0];
905
+ return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
906
+ }
907
+ function buildDetailBreadcrumb(path, suffix = "") {
908
+ if (path.length <= 1) {
909
+ return suffix ? `\u2190 ${suffix}` : path[0] || "";
910
+ }
911
+ const breadcrumb = buildBreadcrumb(path);
912
+ return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
913
+ }
914
+ var init_utils = __esm({
915
+ "src/screen/utils.ts"() {
916
+ "use strict";
917
+ }
918
+ });
919
+
920
+ // src/screen/footer-builder.ts
921
+ function buildFooter(config2 = {}) {
922
+ const {
923
+ navigation = null,
924
+ actions = null,
925
+ info = null,
926
+ escape = "Esc to go back",
927
+ custom = null
928
+ } = config2;
929
+ const lines = [];
930
+ const mainParts = [];
931
+ if (navigation) {
932
+ mainParts.push(navigation);
933
+ }
934
+ if (actions) {
935
+ mainParts.push(actions);
936
+ }
937
+ if (escape) {
938
+ mainParts.push(escape);
939
+ }
940
+ if (mainParts.length > 0) {
941
+ lines.push(mainParts.join(", "));
942
+ }
943
+ if (info) {
944
+ const infoLines = Array.isArray(info) ? info : [info];
945
+ lines.push(...infoLines);
946
+ }
947
+ if (custom) {
948
+ const customLines = Array.isArray(custom) ? custom : [custom];
949
+ lines.push(...customLines);
950
+ }
951
+ return lines;
952
+ }
953
+ function organizeFooterMessages(messages) {
954
+ if (!messages || messages.length === 0) {
955
+ return ["Esc to go back"];
956
+ }
957
+ const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
958
+ const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
959
+ const escape = messages.filter((m) => m.includes("Esc"));
960
+ const others = messages.filter(
961
+ (m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
962
+ );
963
+ const lines = [];
964
+ const mainLine = [...navigation, ...actions, ...escape].join(", ");
965
+ if (mainLine) lines.push(mainLine);
966
+ lines.push(...others);
967
+ return lines;
968
+ }
969
+ var FooterPresets;
970
+ var init_footer_builder = __esm({
971
+ "src/screen/footer-builder.ts"() {
972
+ "use strict";
973
+ FooterPresets = {
974
+ /**
975
+ * Menu screen footer
976
+ */
977
+ menu: (customInfo = null) => buildFooter({
978
+ navigation: "\u2191/\u2193 to navigate",
979
+ actions: "Enter to select",
980
+ escape: "Esc to go back",
981
+ info: customInfo
982
+ }),
983
+ /**
984
+ * Word grid footer
985
+ */
986
+ wordGrid: (totalWords) => buildFooter({
987
+ navigation: "\u2191\u2193\u2190\u2192 to navigate",
988
+ actions: "Enter to select",
989
+ escape: "Esc to go back",
990
+ info: `Total: ${totalWords} words`
991
+ }),
992
+ /**
993
+ * Text input footer
994
+ */
995
+ textInput: () => buildFooter({
996
+ actions: "Type and press Enter to submit",
997
+ escape: "Esc to cancel"
998
+ }),
999
+ /**
1000
+ * Info/static screen footer
1001
+ */
1002
+ info: () => buildFooter({
1003
+ escape: "Esc to continue"
1004
+ }),
1005
+ /**
1006
+ * Main menu footer (escape exits)
1007
+ */
1008
+ mainMenu: () => buildFooter({
1009
+ navigation: "\u2191/\u2193 to navigate",
1010
+ actions: "Enter to select",
1011
+ escape: "Esc to exit"
1012
+ }),
1013
+ /**
1014
+ * Action menu footer (for word cards, etc.)
1015
+ */
1016
+ actionMenu: (hasAudio = false) => {
1017
+ const parts = buildFooter({
1018
+ navigation: "\u2191/\u2193 to navigate",
1019
+ actions: "Enter to select",
1020
+ escape: "Esc to go back"
1021
+ });
1022
+ if (hasAudio) {
1023
+ parts.push("Audio available");
1024
+ }
1025
+ return parts;
1026
+ }
1027
+ };
1028
+ }
1029
+ });
1030
+
1031
+ // src/screen/index.ts
1032
+ var screen_exports = {};
1033
+ __export(screen_exports, {
1034
+ Box: () => Box5,
1035
+ Divider: () => Divider,
1036
+ FooterPresets: () => FooterPresets,
1037
+ GridCell: () => GridCell,
1038
+ InputField: () => InputField,
1039
+ ListComponent: () => ListComponent,
1040
+ ListItem: () => ListItem,
1041
+ MultiColumnListComponent: () => MultiColumnListComponent,
1042
+ MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1043
+ React: () => React5,
1044
+ ScreenBody: () => ScreenBody,
1045
+ ScreenContainer: () => ScreenContainer,
1046
+ ScreenDivider: () => ScreenDivider,
1047
+ ScreenFooter: () => ScreenFooter,
1048
+ ScreenRow: () => ScreenRow,
1049
+ ScreenTitle: () => ScreenTitle,
1050
+ Text: () => Text5,
1051
+ TextBlock: () => TextBlock,
1052
+ buildBreadcrumb: () => buildBreadcrumb,
1053
+ buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1054
+ buildFooter: () => buildFooter,
1055
+ h: () => createElement2,
1056
+ load: () => load,
1057
+ organizeFooterMessages: () => organizeFooterMessages,
1058
+ showListScreen: () => showListScreen,
1059
+ showMenuScreen: () => showMenuScreen,
1060
+ showMultiColumnListScreen: () => showMultiColumnListScreen,
1061
+ showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1062
+ showScreen: () => showScreen,
1063
+ showWordGridScreen: () => showWordGridScreen,
1064
+ useCallback: () => useCallback,
1065
+ useEffect: () => useEffect3,
1066
+ useInput: () => useInput2,
1067
+ useMemo: () => useMemo,
1068
+ useRef: () => useRef3,
1069
+ useState: () => useState3
1070
+ });
1071
+ import React5, { useState as useState3, useEffect as useEffect3, useRef as useRef3, useMemo, useCallback, createElement as createElement2 } from "react";
1072
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
1073
+ async function load() {
1074
+ if (loadPromise) return loadPromise;
1075
+ loadPromise = Promise.all([
1076
+ import("react"),
1077
+ import("ink")
1078
+ ]).then(() => {
1079
+ });
1080
+ return loadPromise;
1081
+ }
1082
+ var loadPromise;
1083
+ var init_screen = __esm({
1084
+ "src/screen/index.ts"() {
1085
+ "use strict";
1086
+ init_screens();
1087
+ init_list_components();
1088
+ init_components();
1089
+ init_ui_elements();
1090
+ init_utils();
1091
+ init_footer_builder();
1092
+ loadPromise = null;
1093
+ if (typeof window === "undefined") {
1094
+ load().catch(() => {
1095
+ });
1096
+ }
1097
+ }
1098
+ });
7
1099
 
8
1100
  // src/args/index.ts
9
1101
  import { readFileSync, existsSync } from "fs";
@@ -1023,6 +2115,18 @@ async function init(flow, opts = {}) {
1023
2115
  let stop = false;
1024
2116
  let context = null;
1025
2117
  try {
2118
+ try {
2119
+ const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
2120
+ if (screenModule && typeof screenModule.load === "function") {
2121
+ await screenModule.load();
2122
+ }
2123
+ } catch {
2124
+ if (typeof __require !== "undefined") {
2125
+ try {
2126
+ } catch {
2127
+ }
2128
+ }
2129
+ }
1026
2130
  context = setup(opts);
1027
2131
  context.isStop = () => stop;
1028
2132
  context = await setupModules(context, opts);