cli-menu-kit 0.1.26 → 0.2.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/api.d.ts +23 -5
- package/dist/api.js +16 -4
- package/dist/component-factories.d.ts +59 -0
- package/dist/component-factories.js +141 -0
- package/dist/components/display/header-v2.d.ts +13 -0
- package/dist/components/display/header-v2.js +43 -0
- package/dist/components/display/header.d.ts +40 -0
- package/dist/components/display/header.js +331 -18
- package/dist/components/display/headers.d.ts +1 -0
- package/dist/components/display/headers.js +15 -5
- package/dist/components/display/hints-v2.d.ts +10 -0
- package/dist/components/display/hints-v2.js +34 -0
- package/dist/components/display/hints.d.ts +56 -0
- package/dist/components/display/hints.js +81 -0
- package/dist/components/display/index.d.ts +4 -1
- package/dist/components/display/index.js +17 -1
- package/dist/components/display/input-prompt.d.ts +35 -0
- package/dist/components/display/input-prompt.js +36 -0
- package/dist/components/display/list.d.ts +49 -0
- package/dist/components/display/list.js +86 -0
- package/dist/components/display/messages.js +5 -5
- package/dist/components/display/progress.d.ts +17 -0
- package/dist/components/display/progress.js +18 -0
- package/dist/components/display/summary.js +72 -10
- package/dist/components/display/table.d.ts +44 -0
- package/dist/components/display/table.js +108 -0
- package/dist/components/inputs/language-input.js +8 -5
- package/dist/components/inputs/number-input.js +19 -14
- package/dist/components/inputs/text-input.js +50 -13
- package/dist/components/menus/boolean-menu.js +34 -20
- package/dist/components/menus/checkbox-menu.d.ts +2 -1
- package/dist/components/menus/checkbox-menu.js +35 -61
- package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
- package/dist/components/menus/checkbox-table-menu.js +398 -0
- package/dist/components/menus/index.d.ts +1 -0
- package/dist/components/menus/index.js +3 -1
- package/dist/components/menus/radio-menu-split.d.ts +34 -0
- package/dist/components/menus/radio-menu-split.js +258 -0
- package/dist/components/menus/radio-menu-v2.d.ts +11 -0
- package/dist/components/menus/radio-menu-v2.js +150 -0
- package/dist/components/menus/radio-menu.d.ts +2 -1
- package/dist/components/menus/radio-menu.js +100 -134
- package/dist/components.js +3 -3
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +21 -0
- package/dist/config/language-config.d.ts +73 -0
- package/dist/config/language-config.js +157 -0
- package/dist/config/user-config.d.ts +83 -0
- package/dist/config/user-config.js +185 -0
- package/dist/core/colors.d.ts +24 -18
- package/dist/core/colors.js +74 -7
- package/dist/core/hint-manager.d.ts +29 -0
- package/dist/core/hint-manager.js +65 -0
- package/dist/core/renderer.d.ts +2 -1
- package/dist/core/renderer.js +46 -22
- package/dist/core/screen-manager.d.ts +54 -0
- package/dist/core/screen-manager.js +119 -0
- package/dist/core/state-manager.d.ts +27 -0
- package/dist/core/state-manager.js +56 -0
- package/dist/core/terminal.d.ts +17 -1
- package/dist/core/terminal.js +124 -4
- package/dist/core/virtual-scroll.d.ts +65 -0
- package/dist/core/virtual-scroll.js +120 -0
- package/dist/features/commands.js +23 -22
- package/dist/i18n/languages/en.js +4 -1
- package/dist/i18n/languages/zh.js +4 -1
- package/dist/i18n/registry.d.ts +4 -3
- package/dist/i18n/registry.js +12 -4
- package/dist/i18n/types.d.ts +3 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +49 -4
- package/dist/layout.d.ts +67 -0
- package/dist/layout.js +86 -0
- package/dist/page-layout.d.ts +123 -0
- package/dist/page-layout.js +195 -0
- package/dist/types/input.types.d.ts +8 -0
- package/dist/types/menu.types.d.ts +61 -5
- package/package.json +4 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RadioMenu - Split rendering and interaction
|
|
4
|
+
* Supports Page Layout V2 architecture with separate render/interact phases
|
|
5
|
+
* Uses ScreenManager for independent region updates
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.renderRadioMenuUI = renderRadioMenuUI;
|
|
9
|
+
exports.waitForRadioMenuInput = waitForRadioMenuInput;
|
|
10
|
+
const terminal_js_1 = require("../../core/terminal.js");
|
|
11
|
+
const keyboard_js_1 = require("../../core/keyboard.js");
|
|
12
|
+
const renderer_js_1 = require("../../core/renderer.js");
|
|
13
|
+
const colors_js_1 = require("../../core/colors.js");
|
|
14
|
+
const registry_js_1 = require("../../i18n/registry.js");
|
|
15
|
+
/**
|
|
16
|
+
* Render radio menu UI (non-blocking)
|
|
17
|
+
* Returns state for later interaction
|
|
18
|
+
*/
|
|
19
|
+
function renderRadioMenuUI(config) {
|
|
20
|
+
const { options, title, prompt, defaultIndex = 0, separatorWidth = 30 } = config;
|
|
21
|
+
// Use i18n for default prompt if not provided
|
|
22
|
+
const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
|
|
23
|
+
// Validate options
|
|
24
|
+
if (!options || options.length === 0) {
|
|
25
|
+
throw new Error('RadioMenu requires at least one option');
|
|
26
|
+
}
|
|
27
|
+
// Initialize state
|
|
28
|
+
let selectedIndex = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
29
|
+
// Separate selectable options from separators
|
|
30
|
+
const selectableIndices = [];
|
|
31
|
+
const optionData = [];
|
|
32
|
+
options.forEach((opt, index) => {
|
|
33
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
34
|
+
optionData.push({ display: '', value: '', isSeparator: true, label: opt.label });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
let display;
|
|
38
|
+
let value;
|
|
39
|
+
if (typeof opt === 'string') {
|
|
40
|
+
display = opt;
|
|
41
|
+
value = opt;
|
|
42
|
+
}
|
|
43
|
+
else if ('value' in opt) {
|
|
44
|
+
display = opt.label ?? String(opt.value ?? '');
|
|
45
|
+
value = opt.value ?? opt.label ?? '';
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
display = opt.label ?? '';
|
|
49
|
+
value = opt.label ?? '';
|
|
50
|
+
}
|
|
51
|
+
optionData.push({ display, value, isSeparator: false });
|
|
52
|
+
selectableIndices.push(index);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// Ensure selectedIndex points to a selectable option
|
|
56
|
+
if (!selectableIndices.includes(selectedIndex)) {
|
|
57
|
+
selectedIndex = selectableIndices[0] || 0;
|
|
58
|
+
}
|
|
59
|
+
// Render menu UI NOW (in Phase 1, before terminal initialization)
|
|
60
|
+
let lineCount = 0;
|
|
61
|
+
// Render menu title if provided
|
|
62
|
+
if (title) {
|
|
63
|
+
(0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.uiColors.primary);
|
|
64
|
+
lineCount++;
|
|
65
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
66
|
+
lineCount++;
|
|
67
|
+
}
|
|
68
|
+
// Render options
|
|
69
|
+
optionData.forEach((item, index) => {
|
|
70
|
+
if (item.isSeparator) {
|
|
71
|
+
(0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const numberMatch = item.display.match(/^(\d+)\.\s*/);
|
|
75
|
+
const letterMatch = item.display.match(/^([a-zA-Z])\.\s*/);
|
|
76
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
77
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.display);
|
|
78
|
+
const isCurrent = index === selectedIndex;
|
|
79
|
+
const displayValue = isExitOption && !isCurrent
|
|
80
|
+
? `${colors_js_1.uiColors.error}${item.display}${colors_js_1.colors.reset}`
|
|
81
|
+
: item.display;
|
|
82
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, isCurrent, prefix);
|
|
83
|
+
const nextIndex = index + 1;
|
|
84
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
85
|
+
(0, terminal_js_1.writeLine)('');
|
|
86
|
+
lineCount++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lineCount++;
|
|
90
|
+
});
|
|
91
|
+
// Don't render input prompt here - it should be in footer
|
|
92
|
+
// Just render the menu options
|
|
93
|
+
// Generate a unique region ID for this menu
|
|
94
|
+
const regionId = `menu-${Date.now()}`;
|
|
95
|
+
// Store the line count for later use in interact phase
|
|
96
|
+
return {
|
|
97
|
+
config,
|
|
98
|
+
selectedIndex,
|
|
99
|
+
selectableIndices,
|
|
100
|
+
optionData,
|
|
101
|
+
terminalState: null, // Will be initialized in interact phase
|
|
102
|
+
displayPrompt,
|
|
103
|
+
initialLineCount: lineCount,
|
|
104
|
+
regionId
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wait for user input and return result (blocking)
|
|
109
|
+
*/
|
|
110
|
+
async function waitForRadioMenuInput(menuState) {
|
|
111
|
+
const { config, selectableIndices, optionData, displayPrompt } = menuState;
|
|
112
|
+
let selectedIndex = menuState.selectedIndex;
|
|
113
|
+
// Initialize terminal NOW (in interact phase, after all rendering is done)
|
|
114
|
+
const state = (0, terminal_js_1.initTerminal)();
|
|
115
|
+
const { allowNumberKeys = true, allowLetterKeys = false, onExit, preserveOnSelect = false } = config;
|
|
116
|
+
// Helper function to get next/previous selectable index
|
|
117
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
118
|
+
let nextIndex = currentIndex;
|
|
119
|
+
const maxAttempts = optionData.length;
|
|
120
|
+
let attempts = 0;
|
|
121
|
+
do {
|
|
122
|
+
if (direction === 'up') {
|
|
123
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : optionData.length - 1;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
nextIndex = nextIndex < optionData.length - 1 ? nextIndex + 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
attempts++;
|
|
129
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
130
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
131
|
+
};
|
|
132
|
+
// Render function (updates display using ScreenManager)
|
|
133
|
+
const render = () => {
|
|
134
|
+
// Move to menu region and clear it
|
|
135
|
+
// screenManager.moveTo(menuState.regionId);
|
|
136
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
137
|
+
let lineCount = 0;
|
|
138
|
+
// Render menu title if provided
|
|
139
|
+
if (config.title) {
|
|
140
|
+
(0, renderer_js_1.renderHeader)(` ${config.title}`, colors_js_1.uiColors.primary);
|
|
141
|
+
lineCount++;
|
|
142
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
143
|
+
lineCount++;
|
|
144
|
+
}
|
|
145
|
+
// Render options
|
|
146
|
+
optionData.forEach((item, index) => {
|
|
147
|
+
if (item.isSeparator) {
|
|
148
|
+
(0, renderer_js_1.renderSectionLabel)(item.label, config.separatorWidth || 30);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const numberMatch = item.display.match(/^(\d+)\.\s*/);
|
|
152
|
+
const letterMatch = item.display.match(/^([a-zA-Z])\.\s*/);
|
|
153
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
154
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.display);
|
|
155
|
+
const isCurrent = index === selectedIndex;
|
|
156
|
+
const displayValue = isExitOption && !isCurrent
|
|
157
|
+
? `${colors_js_1.uiColors.error}${item.display}${colors_js_1.colors.reset}`
|
|
158
|
+
: item.display;
|
|
159
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, isCurrent, prefix);
|
|
160
|
+
const nextIndex = index + 1;
|
|
161
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
162
|
+
(0, terminal_js_1.writeLine)('');
|
|
163
|
+
lineCount++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
lineCount++;
|
|
167
|
+
});
|
|
168
|
+
// Update region size if it changed
|
|
169
|
+
// if (lineCount !== menuState.initialLineCount) {
|
|
170
|
+
// screenManager.updateRegionSize(menuState.regionId, lineCount);
|
|
171
|
+
// menuState.initialLineCount = lineCount;
|
|
172
|
+
// }
|
|
173
|
+
// Update global state with current selection
|
|
174
|
+
// const currentItem = optionData[selectedIndex];
|
|
175
|
+
// if (currentItem && !currentItem.isSeparator) {
|
|
176
|
+
// const match = currentItem.value.match(/^([^.]+)\./);
|
|
177
|
+
// const displayValue = match ? match[1] : String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
178
|
+
// globalState.setState('menu.selectedValue', displayValue);
|
|
179
|
+
// globalState.setState('menu.selectedIndex', selectedIndex);
|
|
180
|
+
// }
|
|
181
|
+
};
|
|
182
|
+
// Register the menu region with ScreenManager
|
|
183
|
+
// if (menuState.initialLineCount) {
|
|
184
|
+
// screenManager.registerRegion(menuState.regionId, menuState.initialLineCount);
|
|
185
|
+
// }
|
|
186
|
+
// Don't render initially - menu was already rendered in Phase 1
|
|
187
|
+
// Just initialize the state
|
|
188
|
+
state.renderedLines = menuState.initialLineCount || 0;
|
|
189
|
+
// Handle keyboard input
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
const onData = (key) => {
|
|
192
|
+
// Handle Ctrl+C
|
|
193
|
+
if ((0, keyboard_js_1.isCtrlC)(key)) {
|
|
194
|
+
state.stdin.removeListener('data', onData);
|
|
195
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
196
|
+
(0, terminal_js_1.restoreTerminal)(state);
|
|
197
|
+
if (onExit) {
|
|
198
|
+
onExit();
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
|
|
202
|
+
}
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
// Handle arrow keys
|
|
206
|
+
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
207
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
|
|
208
|
+
render();
|
|
209
|
+
}
|
|
210
|
+
else if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
211
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
|
|
212
|
+
render();
|
|
213
|
+
}
|
|
214
|
+
// Handle Enter
|
|
215
|
+
if ((0, keyboard_js_1.isEnter)(key)) {
|
|
216
|
+
const selectedItem = optionData[selectedIndex];
|
|
217
|
+
if (selectedItem && !selectedItem.isSeparator) {
|
|
218
|
+
state.stdin.removeListener('data', onData);
|
|
219
|
+
if (!preserveOnSelect) {
|
|
220
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
221
|
+
}
|
|
222
|
+
(0, terminal_js_1.restoreTerminal)(state);
|
|
223
|
+
resolve({
|
|
224
|
+
value: selectedItem.value,
|
|
225
|
+
index: selectableIndices.indexOf(selectedIndex)
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Handle number keys
|
|
230
|
+
if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
|
|
231
|
+
const num = parseInt(key, 10);
|
|
232
|
+
const targetIndex = num === 0 ? 9 : num - 1;
|
|
233
|
+
if (targetIndex < selectableIndices.length) {
|
|
234
|
+
selectedIndex = selectableIndices[targetIndex];
|
|
235
|
+
render();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Handle letter keys
|
|
239
|
+
if (allowLetterKeys) {
|
|
240
|
+
const letter = (0, keyboard_js_1.normalizeLetter)(key);
|
|
241
|
+
if (letter) {
|
|
242
|
+
const matchingIndex = selectableIndices.find(idx => {
|
|
243
|
+
const item = optionData[idx];
|
|
244
|
+
if (!item || item.isSeparator)
|
|
245
|
+
return false;
|
|
246
|
+
const normalized = (0, keyboard_js_1.normalizeLetter)(item.display.charAt(0));
|
|
247
|
+
return normalized === letter;
|
|
248
|
+
});
|
|
249
|
+
if (matchingIndex !== undefined) {
|
|
250
|
+
selectedIndex = matchingIndex;
|
|
251
|
+
render();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
state.stdin.on('data', onData);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radio Menu Component - New Architecture Version
|
|
3
|
+
* Renders menu directly in new architecture without calling old showRadioMenu
|
|
4
|
+
*/
|
|
5
|
+
import { Component } from '../../layout.js';
|
|
6
|
+
import type { RadioMenuConfig, RadioMenuResult } from '../../types/menu.types.js';
|
|
7
|
+
export interface RadioMenuComponentConfig {
|
|
8
|
+
menuConfig: RadioMenuConfig;
|
|
9
|
+
onResult: (result: RadioMenuResult) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createRadioMenuComponentV2(config: RadioMenuComponentConfig): Component;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Radio Menu Component - New Architecture Version
|
|
4
|
+
* Renders menu directly in new architecture without calling old showRadioMenu
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.createRadioMenuComponentV2 = createRadioMenuComponentV2;
|
|
11
|
+
const readline_1 = __importDefault(require("readline"));
|
|
12
|
+
const layout_js_1 = require("../../layout.js");
|
|
13
|
+
const colors_js_1 = require("../../core/colors.js");
|
|
14
|
+
function createRadioMenuComponentV2(config) {
|
|
15
|
+
let selectedIndex = 0;
|
|
16
|
+
const options = config.menuConfig.options;
|
|
17
|
+
// Parse options
|
|
18
|
+
const selectableIndices = [];
|
|
19
|
+
const optionData = [];
|
|
20
|
+
options.forEach((opt, index) => {
|
|
21
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
22
|
+
optionData.push({ value: '', isSeparator: true, label: opt.label });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const value = typeof opt === 'string' ? opt : opt.value || '';
|
|
26
|
+
optionData.push({ value, isSeparator: false });
|
|
27
|
+
selectableIndices.push(index);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Render function
|
|
31
|
+
const renderMenu = (rect) => {
|
|
32
|
+
const lines = [];
|
|
33
|
+
optionData.forEach((item, index) => {
|
|
34
|
+
if (item.isSeparator) {
|
|
35
|
+
// Render separator
|
|
36
|
+
const label = item.label || '';
|
|
37
|
+
const line = `${colors_js_1.colors.dim}${'─'.repeat(5)} ${label} ${'─'.repeat(Math.max(0, 20 - label.length))}${colors_js_1.colors.reset}`;
|
|
38
|
+
lines.push(line);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Render option
|
|
42
|
+
const isSelected = index === selectedIndex;
|
|
43
|
+
const prefix = isSelected ? `${colors_js_1.colors.green}>${colors_js_1.colors.reset}` : ' ';
|
|
44
|
+
// Add number prefix
|
|
45
|
+
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
46
|
+
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
47
|
+
const hasPrefix = numberMatch || letterMatch;
|
|
48
|
+
const displayPrefix = hasPrefix ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
49
|
+
// Check if exit option
|
|
50
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
51
|
+
const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
52
|
+
const text = isSelected
|
|
53
|
+
? `${colors_js_1.colors.green}${colors_js_1.colors.bold}${displayPrefix}${displayValue}${colors_js_1.colors.reset}`
|
|
54
|
+
: `${displayPrefix}${displayValue}`;
|
|
55
|
+
lines.push(`${prefix} ${text}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Fill remaining lines
|
|
59
|
+
while (lines.length < rect.height) {
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
type: 'radio-menu',
|
|
66
|
+
regionId: 'main',
|
|
67
|
+
render: renderMenu,
|
|
68
|
+
interact: async () => {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
readline_1.default.emitKeypressEvents(process.stdin);
|
|
71
|
+
if (process.stdin.isTTY)
|
|
72
|
+
process.stdin.setRawMode(true);
|
|
73
|
+
const onKey = async (str, key) => {
|
|
74
|
+
if (key.ctrl && key.name === 'c') {
|
|
75
|
+
cleanup();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
let needsUpdate = false;
|
|
79
|
+
// Arrow key navigation
|
|
80
|
+
if (key.name === 'up') {
|
|
81
|
+
// Move to previous selectable option
|
|
82
|
+
const currentPos = selectableIndices.indexOf(selectedIndex);
|
|
83
|
+
if (currentPos > 0) {
|
|
84
|
+
selectedIndex = selectableIndices[currentPos - 1];
|
|
85
|
+
needsUpdate = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (key.name === 'down') {
|
|
89
|
+
// Move to next selectable option
|
|
90
|
+
const currentPos = selectableIndices.indexOf(selectedIndex);
|
|
91
|
+
if (currentPos < selectableIndices.length - 1) {
|
|
92
|
+
selectedIndex = selectableIndices[currentPos + 1];
|
|
93
|
+
needsUpdate = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Number key selection
|
|
97
|
+
else if (config.menuConfig.allowNumberKeys && str && /^[0-9]$/.test(str)) {
|
|
98
|
+
const num = parseInt(str, 10);
|
|
99
|
+
if (num > 0 && num <= selectableIndices.length) {
|
|
100
|
+
selectedIndex = selectableIndices[num - 1];
|
|
101
|
+
needsUpdate = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Letter key selection
|
|
105
|
+
else if (config.menuConfig.allowLetterKeys && str && /^[a-zA-Z]$/.test(str)) {
|
|
106
|
+
// Find option starting with this letter
|
|
107
|
+
const letter = str.toLowerCase();
|
|
108
|
+
const matchIndex = selectableIndices.find(idx => {
|
|
109
|
+
const value = optionData[idx].value.toLowerCase();
|
|
110
|
+
return value.startsWith(letter) || value.match(new RegExp(`^\\d+\\.\\s*${letter}`, 'i'));
|
|
111
|
+
});
|
|
112
|
+
if (matchIndex !== undefined) {
|
|
113
|
+
selectedIndex = matchIndex;
|
|
114
|
+
needsUpdate = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Enter to confirm
|
|
118
|
+
else if (key.name === 'return') {
|
|
119
|
+
cleanup();
|
|
120
|
+
const result = {
|
|
121
|
+
value: optionData[selectedIndex].value,
|
|
122
|
+
index: selectableIndices.indexOf(selectedIndex)
|
|
123
|
+
};
|
|
124
|
+
await config.onResult(result);
|
|
125
|
+
resolve();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Update display if selection changed
|
|
129
|
+
if (needsUpdate) {
|
|
130
|
+
const layout = (0, layout_js_1.computeLayout)();
|
|
131
|
+
const lines = renderMenu(layout.main);
|
|
132
|
+
layout_js_1.screenManager.renderRegion('main', lines);
|
|
133
|
+
// Update hint
|
|
134
|
+
layout_js_1.hintManager.set('menu', `Selected: ${optionData[selectedIndex].value}`, 10);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function cleanup() {
|
|
138
|
+
process.stdin.off('keypress', onKey);
|
|
139
|
+
if (process.stdin.isTTY)
|
|
140
|
+
process.stdin.setRawMode(false);
|
|
141
|
+
layout_js_1.hintManager.clear('menu');
|
|
142
|
+
}
|
|
143
|
+
process.stdin.on('keypress', onKey);
|
|
144
|
+
// Set initial hint
|
|
145
|
+
layout_js_1.hintManager.set('menu', `Selected: ${optionData[selectedIndex].value}`, 10);
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
config
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -6,6 +6,7 @@ import { RadioMenuConfig, RadioMenuResult } from '../../types/menu.types.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Show a radio menu (single-select)
|
|
8
8
|
* @param config - Menu configuration
|
|
9
|
+
* @param hints - Optional hints to display at the bottom (for Page Layout use)
|
|
9
10
|
* @returns Promise resolving to selected option
|
|
10
11
|
*/
|
|
11
|
-
export declare function showRadioMenu(config: RadioMenuConfig): Promise<RadioMenuResult>;
|
|
12
|
+
export declare function showRadioMenu(config: RadioMenuConfig, hints?: string[]): Promise<RadioMenuResult>;
|