cli-menu-kit 0.1.26 ā 0.2.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/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/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 +3 -0
- package/dist/components/display/index.js +15 -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/table.d.ts +42 -0
- package/dist/components/display/table.js +107 -0
- package/dist/components/menus/boolean-menu.js +2 -1
- package/dist/components/menus/checkbox-menu.d.ts +2 -1
- package/dist/components/menus/checkbox-menu.js +30 -59
- package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
- package/dist/components/menus/checkbox-table-menu.js +395 -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 +33 -0
- package/dist/components/menus/radio-menu-split.js +248 -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 +60 -123
- 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 +22 -6
- 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 +4 -1
- package/dist/core/terminal.js +37 -4
- package/dist/core/virtual-scroll.d.ts +65 -0
- package/dist/core/virtual-scroll.js +120 -0
- 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 +5 -4
- package/dist/index.js +30 -4
- package/dist/layout.d.ts +68 -0
- package/dist/layout.js +134 -0
- package/dist/page-layout.d.ts +92 -0
- package/dist/page-layout.js +156 -0
- package/dist/types/menu.types.d.ts +57 -5
- package/package.json +1 -1
- package/dist/types/layout.types.d.ts +0 -56
- package/dist/types/layout.types.js +0 -36
|
@@ -0,0 +1,248 @@
|
|
|
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({ value: '', isSeparator: true, label: opt.label });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
let value;
|
|
38
|
+
if (typeof opt === 'string') {
|
|
39
|
+
value = opt;
|
|
40
|
+
}
|
|
41
|
+
else if ('value' in opt) {
|
|
42
|
+
value = opt.value ?? opt.label ?? '';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
value = opt.label ?? '';
|
|
46
|
+
}
|
|
47
|
+
optionData.push({ value, isSeparator: false });
|
|
48
|
+
selectableIndices.push(index);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// Ensure selectedIndex points to a selectable option
|
|
52
|
+
if (!selectableIndices.includes(selectedIndex)) {
|
|
53
|
+
selectedIndex = selectableIndices[0] || 0;
|
|
54
|
+
}
|
|
55
|
+
// Render menu UI NOW (in Phase 1, before terminal initialization)
|
|
56
|
+
let lineCount = 0;
|
|
57
|
+
// Render menu title if provided
|
|
58
|
+
if (title) {
|
|
59
|
+
(0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.colors.cyan);
|
|
60
|
+
lineCount++;
|
|
61
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
62
|
+
lineCount++;
|
|
63
|
+
}
|
|
64
|
+
// Render options
|
|
65
|
+
optionData.forEach((item, index) => {
|
|
66
|
+
if (item.isSeparator) {
|
|
67
|
+
(0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
71
|
+
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
72
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
73
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
74
|
+
const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
75
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
|
|
76
|
+
const nextIndex = index + 1;
|
|
77
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
78
|
+
(0, terminal_js_1.writeLine)('');
|
|
79
|
+
lineCount++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
lineCount++;
|
|
83
|
+
});
|
|
84
|
+
// Don't render input prompt here - it should be in footer
|
|
85
|
+
// Just render the menu options
|
|
86
|
+
// Generate a unique region ID for this menu
|
|
87
|
+
const regionId = `menu-${Date.now()}`;
|
|
88
|
+
// Store the line count for later use in interact phase
|
|
89
|
+
return {
|
|
90
|
+
config,
|
|
91
|
+
selectedIndex,
|
|
92
|
+
selectableIndices,
|
|
93
|
+
optionData,
|
|
94
|
+
terminalState: null, // Will be initialized in interact phase
|
|
95
|
+
displayPrompt,
|
|
96
|
+
initialLineCount: lineCount,
|
|
97
|
+
regionId
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wait for user input and return result (blocking)
|
|
102
|
+
*/
|
|
103
|
+
async function waitForRadioMenuInput(menuState) {
|
|
104
|
+
const { config, selectableIndices, optionData, displayPrompt } = menuState;
|
|
105
|
+
let selectedIndex = menuState.selectedIndex;
|
|
106
|
+
// Initialize terminal NOW (in interact phase, after all rendering is done)
|
|
107
|
+
const state = (0, terminal_js_1.initTerminal)();
|
|
108
|
+
const { allowNumberKeys = true, allowLetterKeys = false, onExit, preserveOnSelect = false } = config;
|
|
109
|
+
// Helper function to get next/previous selectable index
|
|
110
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
111
|
+
let nextIndex = currentIndex;
|
|
112
|
+
const maxAttempts = optionData.length;
|
|
113
|
+
let attempts = 0;
|
|
114
|
+
do {
|
|
115
|
+
if (direction === 'up') {
|
|
116
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : optionData.length - 1;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
nextIndex = nextIndex < optionData.length - 1 ? nextIndex + 1 : 0;
|
|
120
|
+
}
|
|
121
|
+
attempts++;
|
|
122
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
123
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
124
|
+
};
|
|
125
|
+
// Render function (updates display using ScreenManager)
|
|
126
|
+
const render = () => {
|
|
127
|
+
// Move to menu region and clear it
|
|
128
|
+
// screenManager.moveTo(menuState.regionId);
|
|
129
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
130
|
+
let lineCount = 0;
|
|
131
|
+
// Render menu title if provided
|
|
132
|
+
if (config.title) {
|
|
133
|
+
(0, renderer_js_1.renderHeader)(` ${config.title}`, colors_js_1.colors.cyan);
|
|
134
|
+
lineCount++;
|
|
135
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
136
|
+
lineCount++;
|
|
137
|
+
}
|
|
138
|
+
// Render options
|
|
139
|
+
optionData.forEach((item, index) => {
|
|
140
|
+
if (item.isSeparator) {
|
|
141
|
+
(0, renderer_js_1.renderSectionLabel)(item.label, config.separatorWidth || 30);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
145
|
+
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
146
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
147
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
148
|
+
const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
149
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
|
|
150
|
+
const nextIndex = index + 1;
|
|
151
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
152
|
+
(0, terminal_js_1.writeLine)('');
|
|
153
|
+
lineCount++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
lineCount++;
|
|
157
|
+
});
|
|
158
|
+
// Update region size if it changed
|
|
159
|
+
// if (lineCount !== menuState.initialLineCount) {
|
|
160
|
+
// screenManager.updateRegionSize(menuState.regionId, lineCount);
|
|
161
|
+
// menuState.initialLineCount = lineCount;
|
|
162
|
+
// }
|
|
163
|
+
// Update global state with current selection
|
|
164
|
+
// const currentItem = optionData[selectedIndex];
|
|
165
|
+
// if (currentItem && !currentItem.isSeparator) {
|
|
166
|
+
// const match = currentItem.value.match(/^([^.]+)\./);
|
|
167
|
+
// const displayValue = match ? match[1] : String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
168
|
+
// globalState.setState('menu.selectedValue', displayValue);
|
|
169
|
+
// globalState.setState('menu.selectedIndex', selectedIndex);
|
|
170
|
+
// }
|
|
171
|
+
};
|
|
172
|
+
// Register the menu region with ScreenManager
|
|
173
|
+
// if (menuState.initialLineCount) {
|
|
174
|
+
// screenManager.registerRegion(menuState.regionId, menuState.initialLineCount);
|
|
175
|
+
// }
|
|
176
|
+
// Don't render initially - menu was already rendered in Phase 1
|
|
177
|
+
// Just initialize the state
|
|
178
|
+
state.renderedLines = menuState.initialLineCount || 0;
|
|
179
|
+
// Handle keyboard input
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
const onData = (key) => {
|
|
182
|
+
// Handle Ctrl+C
|
|
183
|
+
if ((0, keyboard_js_1.isCtrlC)(key)) {
|
|
184
|
+
state.stdin.removeListener('data', onData);
|
|
185
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
186
|
+
(0, terminal_js_1.restoreTerminal)(state);
|
|
187
|
+
if (onExit) {
|
|
188
|
+
onExit();
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.log('\nš åč§!');
|
|
192
|
+
}
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
// Handle arrow keys
|
|
196
|
+
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
197
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
|
|
198
|
+
render();
|
|
199
|
+
}
|
|
200
|
+
else if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
201
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
|
|
202
|
+
render();
|
|
203
|
+
}
|
|
204
|
+
// Handle Enter
|
|
205
|
+
if ((0, keyboard_js_1.isEnter)(key)) {
|
|
206
|
+
const selectedItem = optionData[selectedIndex];
|
|
207
|
+
if (selectedItem && !selectedItem.isSeparator) {
|
|
208
|
+
state.stdin.removeListener('data', onData);
|
|
209
|
+
if (!preserveOnSelect) {
|
|
210
|
+
// screenManager.clearRegion(menuState.regionId);
|
|
211
|
+
}
|
|
212
|
+
(0, terminal_js_1.restoreTerminal)(state);
|
|
213
|
+
resolve({
|
|
214
|
+
value: selectedItem.value,
|
|
215
|
+
index: selectableIndices.indexOf(selectedIndex)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Handle number keys
|
|
220
|
+
if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
|
|
221
|
+
const num = parseInt(key, 10);
|
|
222
|
+
const targetIndex = num === 0 ? 9 : num - 1;
|
|
223
|
+
if (targetIndex < selectableIndices.length) {
|
|
224
|
+
selectedIndex = selectableIndices[targetIndex];
|
|
225
|
+
render();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Handle letter keys
|
|
229
|
+
if (allowLetterKeys) {
|
|
230
|
+
const letter = (0, keyboard_js_1.normalizeLetter)(key);
|
|
231
|
+
if (letter) {
|
|
232
|
+
const matchingIndex = selectableIndices.find(idx => {
|
|
233
|
+
const item = optionData[idx];
|
|
234
|
+
if (!item || item.isSeparator)
|
|
235
|
+
return false;
|
|
236
|
+
const normalized = (0, keyboard_js_1.normalizeLetter)(item.value.charAt(0));
|
|
237
|
+
return normalized === letter;
|
|
238
|
+
});
|
|
239
|
+
if (matchingIndex !== undefined) {
|
|
240
|
+
selectedIndex = matchingIndex;
|
|
241
|
+
render();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
state.stdin.on('data', onData);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -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>;
|
|
@@ -5,61 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.showRadioMenu = showRadioMenu;
|
|
8
|
-
const layout_types_js_1 = require("../../types/layout.types.js");
|
|
9
8
|
const terminal_js_1 = require("../../core/terminal.js");
|
|
10
9
|
const keyboard_js_1 = require("../../core/keyboard.js");
|
|
11
10
|
const renderer_js_1 = require("../../core/renderer.js");
|
|
11
|
+
const renderer_js_2 = require("../../core/renderer.js");
|
|
12
12
|
const colors_js_1 = require("../../core/colors.js");
|
|
13
13
|
const registry_js_1 = require("../../i18n/registry.js");
|
|
14
|
-
/**
|
|
15
|
-
* Generate hints based on menu configuration and actual options
|
|
16
|
-
*/
|
|
17
|
-
function generateHints(options, allowNumberKeys, allowLetterKeys) {
|
|
18
|
-
const hints = [];
|
|
19
|
-
// Count selectable options
|
|
20
|
-
const selectableCount = options.filter(opt => !(typeof opt === 'object' && 'type' in opt && opt.type === 'separator')).length;
|
|
21
|
-
// Only show arrow hints if there are multiple options
|
|
22
|
-
if (selectableCount > 1) {
|
|
23
|
-
hints.push((0, registry_js_1.t)('hints.arrows'));
|
|
24
|
-
}
|
|
25
|
-
// Check if there are actually number-prefixed options
|
|
26
|
-
if (allowNumberKeys) {
|
|
27
|
-
const hasNumberOptions = options.some(opt => {
|
|
28
|
-
if (typeof opt === 'string') {
|
|
29
|
-
return /^\d+\./.test(opt);
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
});
|
|
33
|
-
if (hasNumberOptions) {
|
|
34
|
-
hints.push((0, registry_js_1.t)('hints.numbers'));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// Check if there are actually letter-prefixed options
|
|
38
|
-
if (allowLetterKeys) {
|
|
39
|
-
const hasLetterOptions = options.some(opt => {
|
|
40
|
-
if (typeof opt === 'string') {
|
|
41
|
-
return /^[a-zA-Z]\./.test(opt);
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
});
|
|
45
|
-
if (hasLetterOptions) {
|
|
46
|
-
hints.push((0, registry_js_1.t)('hints.letters'));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
hints.push((0, registry_js_1.t)('hints.enter'));
|
|
50
|
-
return hints;
|
|
51
|
-
}
|
|
52
14
|
/**
|
|
53
15
|
* Show a radio menu (single-select)
|
|
54
16
|
* @param config - Menu configuration
|
|
17
|
+
* @param hints - Optional hints to display at the bottom (for Page Layout use)
|
|
55
18
|
* @returns Promise resolving to selected option
|
|
56
19
|
*/
|
|
57
|
-
async function showRadioMenu(config) {
|
|
58
|
-
const { options, title, prompt,
|
|
20
|
+
async function showRadioMenu(config, hints) {
|
|
21
|
+
const { options, title, prompt, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, separatorWidth = 30, onExit, preserveOnSelect = false } = config;
|
|
59
22
|
// Use i18n for default prompt if not provided
|
|
60
23
|
const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
|
|
61
|
-
// Generate hints dynamically if not provided
|
|
62
|
-
const displayHints = hints || generateHints(options, allowNumberKeys, allowLetterKeys);
|
|
63
24
|
// Validate options
|
|
64
25
|
if (!options || options.length === 0) {
|
|
65
26
|
throw new Error('RadioMenu requires at least one option');
|
|
@@ -89,8 +50,6 @@ async function showRadioMenu(config) {
|
|
|
89
50
|
selectableIndices.push(index);
|
|
90
51
|
}
|
|
91
52
|
});
|
|
92
|
-
// Use MINIMAL layout for single-option menus
|
|
93
|
-
const effectiveLayout = selectableIndices.length === 1 ? layout_types_js_1.LAYOUT_PRESETS.MINIMAL : layout;
|
|
94
53
|
// Ensure selectedIndex points to a selectable option
|
|
95
54
|
if (!selectableIndices.includes(selectedIndex)) {
|
|
96
55
|
selectedIndex = selectableIndices[0] || 0;
|
|
@@ -115,80 +74,63 @@ async function showRadioMenu(config) {
|
|
|
115
74
|
const render = () => {
|
|
116
75
|
(0, terminal_js_1.clearMenu)(state);
|
|
117
76
|
let lineCount = 0;
|
|
118
|
-
// Render
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lineCount++;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
case 'options':
|
|
134
|
-
optionData.forEach((item, index) => {
|
|
135
|
-
if (item.isSeparator) {
|
|
136
|
-
// Render section label with configured width
|
|
137
|
-
(0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
// Check if option starts with a number or letter prefix
|
|
141
|
-
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
142
|
-
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
143
|
-
// Don't add prefix if option already has number or letter prefix
|
|
144
|
-
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
145
|
-
// Check if this is an Exit option (contains "Exit" or "Quit")
|
|
146
|
-
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
147
|
-
const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
148
|
-
// For radio menus, don't show selection indicator (pass undefined instead of false)
|
|
149
|
-
(0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
|
|
150
|
-
// Add blank line after last item before next separator
|
|
151
|
-
const nextIndex = index + 1;
|
|
152
|
-
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
153
|
-
(0, terminal_js_1.writeLine)('');
|
|
154
|
-
lineCount++; // Count the blank line
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
lineCount++;
|
|
158
|
-
});
|
|
159
|
-
break;
|
|
160
|
-
case 'input':
|
|
161
|
-
if (effectiveLayout.visible.input) {
|
|
162
|
-
// Calculate display value (current selection number)
|
|
163
|
-
let displayValue = '';
|
|
164
|
-
const currentItem = optionData[selectedIndex];
|
|
165
|
-
if (currentItem && !currentItem.isSeparator) {
|
|
166
|
-
const match = currentItem.value.match(/^([^.]+)\./);
|
|
167
|
-
if (match) {
|
|
168
|
-
displayValue = match[1];
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
(0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
|
|
175
|
-
lineCount++;
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
case 'hints':
|
|
179
|
-
if (effectiveLayout.visible.hints && displayHints.length > 0) {
|
|
180
|
-
(0, renderer_js_1.renderHints)(displayHints);
|
|
181
|
-
lineCount++;
|
|
182
|
-
}
|
|
183
|
-
break;
|
|
77
|
+
// Render title if provided
|
|
78
|
+
if (title) {
|
|
79
|
+
(0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.colors.cyan);
|
|
80
|
+
lineCount++;
|
|
81
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
82
|
+
lineCount++;
|
|
83
|
+
}
|
|
84
|
+
// Render options
|
|
85
|
+
optionData.forEach((item, index) => {
|
|
86
|
+
if (item.isSeparator) {
|
|
87
|
+
// Render section label with configured width
|
|
88
|
+
(0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
|
|
184
89
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
90
|
+
else {
|
|
91
|
+
// Check if option starts with a number or letter prefix
|
|
92
|
+
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
93
|
+
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
94
|
+
// Don't add prefix if option already has number or letter prefix
|
|
95
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
96
|
+
// Check if this is an Exit option (contains "Exit" or "Quit")
|
|
97
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
98
|
+
const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
99
|
+
// For radio menus, don't show selection indicator (pass undefined instead of false)
|
|
100
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
|
|
101
|
+
// Add blank line after last item before next separator
|
|
102
|
+
const nextIndex = index + 1;
|
|
103
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
104
|
+
(0, terminal_js_1.writeLine)('');
|
|
105
|
+
lineCount++; // Count the blank line
|
|
106
|
+
}
|
|
190
107
|
}
|
|
108
|
+
lineCount++;
|
|
191
109
|
});
|
|
110
|
+
// Render input prompt
|
|
111
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
112
|
+
lineCount++;
|
|
113
|
+
// Calculate display value (current selection number)
|
|
114
|
+
let displayValue = '';
|
|
115
|
+
const currentItem = optionData[selectedIndex];
|
|
116
|
+
if (currentItem && !currentItem.isSeparator) {
|
|
117
|
+
const match = currentItem.value.match(/^([^.]+)\./);
|
|
118
|
+
if (match) {
|
|
119
|
+
displayValue = match[1];
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
(0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
|
|
126
|
+
lineCount++;
|
|
127
|
+
// Render hints if provided (for Page Layout footer)
|
|
128
|
+
if (hints && hints.length > 0) {
|
|
129
|
+
(0, renderer_js_1.renderBlankLines)(1);
|
|
130
|
+
lineCount++;
|
|
131
|
+
(0, renderer_js_2.renderHints)(hints);
|
|
132
|
+
lineCount++;
|
|
133
|
+
}
|
|
192
134
|
state.renderedLines = lineCount;
|
|
193
135
|
};
|
|
194
136
|
// Initial render
|
|
@@ -199,14 +141,9 @@ async function showRadioMenu(config) {
|
|
|
199
141
|
// Handle Ctrl+C
|
|
200
142
|
if ((0, keyboard_js_1.isCtrlC)(key)) {
|
|
201
143
|
state.stdin.removeListener('data', onData);
|
|
202
|
-
(0, terminal_js_1.clearMenu)(state);
|
|
203
144
|
(0, terminal_js_1.restoreTerminal)(state);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
console.log('\nš åč§!');
|
|
209
|
-
}
|
|
145
|
+
// Don't clear menu on Ctrl+C - just exit directly
|
|
146
|
+
console.log('\n');
|
|
210
147
|
process.exit(0);
|
|
211
148
|
}
|
|
212
149
|
// Handle Enter
|