cli-menu-kit 0.1.0 → 0.1.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.
|
@@ -24,8 +24,48 @@ async function showCheckboxMenu(config) {
|
|
|
24
24
|
let cursorIndex = 0;
|
|
25
25
|
const selected = new Set(defaultSelected);
|
|
26
26
|
const state = (0, terminal_js_1.initTerminal)();
|
|
27
|
-
//
|
|
28
|
-
const
|
|
27
|
+
// Separate selectable options from separators
|
|
28
|
+
const selectableIndices = [];
|
|
29
|
+
const optionData = [];
|
|
30
|
+
options.forEach((opt, index) => {
|
|
31
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
32
|
+
optionData.push({ value: '', isSeparator: true, label: opt.label });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
let value;
|
|
36
|
+
if (typeof opt === 'string') {
|
|
37
|
+
value = opt;
|
|
38
|
+
}
|
|
39
|
+
else if ('value' in opt) {
|
|
40
|
+
value = opt.value ?? opt.label ?? '';
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
value = opt.label ?? '';
|
|
44
|
+
}
|
|
45
|
+
optionData.push({ value, isSeparator: false });
|
|
46
|
+
selectableIndices.push(index);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Ensure cursorIndex points to a selectable option
|
|
50
|
+
if (!selectableIndices.includes(cursorIndex)) {
|
|
51
|
+
cursorIndex = selectableIndices[0] || 0;
|
|
52
|
+
}
|
|
53
|
+
// Helper function to get next/previous selectable index
|
|
54
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
55
|
+
let nextIndex = currentIndex;
|
|
56
|
+
const maxAttempts = options.length;
|
|
57
|
+
let attempts = 0;
|
|
58
|
+
do {
|
|
59
|
+
if (direction === 'up') {
|
|
60
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
attempts++;
|
|
66
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
67
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
68
|
+
};
|
|
29
69
|
// Render function
|
|
30
70
|
const render = () => {
|
|
31
71
|
(0, terminal_js_1.clearMenu)(state);
|
|
@@ -48,8 +88,14 @@ async function showCheckboxMenu(config) {
|
|
|
48
88
|
}
|
|
49
89
|
break;
|
|
50
90
|
case 'options':
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
optionData.forEach((item, index) => {
|
|
92
|
+
if (item.isSeparator) {
|
|
93
|
+
// Render section label
|
|
94
|
+
(0, renderer_js_1.renderSectionLabel)(item.label);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
(0, renderer_js_1.renderOption)(item.value, selected.has(index), index === cursorIndex);
|
|
98
|
+
}
|
|
53
99
|
lineCount++;
|
|
54
100
|
});
|
|
55
101
|
break;
|
|
@@ -100,42 +146,54 @@ async function showCheckboxMenu(config) {
|
|
|
100
146
|
const indices = Array.from(selected).sort((a, b) => a - b);
|
|
101
147
|
const values = indices.map(i => {
|
|
102
148
|
const option = options[i];
|
|
103
|
-
|
|
149
|
+
if (typeof option === 'string') {
|
|
150
|
+
return option;
|
|
151
|
+
}
|
|
152
|
+
else if ('type' in option && option.type === 'separator') {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
else if ('value' in option) {
|
|
156
|
+
return option.value ?? option.label ?? '';
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
return option.label ?? '';
|
|
160
|
+
}
|
|
104
161
|
});
|
|
105
162
|
resolve({ indices, values });
|
|
106
163
|
return;
|
|
107
164
|
}
|
|
108
165
|
// Handle Space (toggle selection)
|
|
109
166
|
if ((0, keyboard_js_1.isSpace)(key)) {
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
167
|
+
// Only toggle if cursor is on a selectable item
|
|
168
|
+
if (selectableIndices.includes(cursorIndex)) {
|
|
169
|
+
if (selected.has(cursorIndex)) {
|
|
170
|
+
selected.delete(cursorIndex);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Check max selections
|
|
174
|
+
if (!maxSelections || selected.size < maxSelections) {
|
|
175
|
+
selected.add(cursorIndex);
|
|
176
|
+
}
|
|
117
177
|
}
|
|
178
|
+
render();
|
|
118
179
|
}
|
|
119
|
-
render();
|
|
120
180
|
return;
|
|
121
181
|
}
|
|
122
182
|
// Handle arrow keys
|
|
123
183
|
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
124
|
-
cursorIndex = cursorIndex
|
|
184
|
+
cursorIndex = getNextSelectableIndex(cursorIndex, 'up');
|
|
125
185
|
render();
|
|
126
186
|
return;
|
|
127
187
|
}
|
|
128
188
|
if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
129
|
-
cursorIndex = cursorIndex
|
|
189
|
+
cursorIndex = getNextSelectableIndex(cursorIndex, 'down');
|
|
130
190
|
render();
|
|
131
191
|
return;
|
|
132
192
|
}
|
|
133
193
|
// Handle 'A' (select all)
|
|
134
194
|
if (allowSelectAll && (0, keyboard_js_1.normalizeLetter)(key) === 'a') {
|
|
135
|
-
if (!maxSelections || maxSelections >=
|
|
136
|
-
|
|
137
|
-
selected.add(i);
|
|
138
|
-
}
|
|
195
|
+
if (!maxSelections || maxSelections >= selectableIndices.length) {
|
|
196
|
+
selectableIndices.forEach(i => selected.add(i));
|
|
139
197
|
render();
|
|
140
198
|
}
|
|
141
199
|
return;
|
|
@@ -143,13 +201,13 @@ async function showCheckboxMenu(config) {
|
|
|
143
201
|
// Handle 'I' (invert selection)
|
|
144
202
|
if (allowInvert && (0, keyboard_js_1.normalizeLetter)(key) === 'i') {
|
|
145
203
|
const newSelected = new Set();
|
|
146
|
-
|
|
204
|
+
selectableIndices.forEach(i => {
|
|
147
205
|
if (!selected.has(i)) {
|
|
148
206
|
if (!maxSelections || newSelected.size < maxSelections) {
|
|
149
207
|
newSelected.add(i);
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
|
-
}
|
|
210
|
+
});
|
|
153
211
|
selected.clear();
|
|
154
212
|
newSelected.forEach(i => selected.add(i));
|
|
155
213
|
render();
|
|
@@ -24,8 +24,48 @@ async function showRadioMenu(config) {
|
|
|
24
24
|
// Initialize state
|
|
25
25
|
let selectedIndex = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
26
26
|
const state = (0, terminal_js_1.initTerminal)();
|
|
27
|
-
//
|
|
28
|
-
const
|
|
27
|
+
// Separate selectable options from separators
|
|
28
|
+
const selectableIndices = [];
|
|
29
|
+
const optionData = [];
|
|
30
|
+
options.forEach((opt, index) => {
|
|
31
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
32
|
+
optionData.push({ value: '', isSeparator: true, label: opt.label });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
let value;
|
|
36
|
+
if (typeof opt === 'string') {
|
|
37
|
+
value = opt;
|
|
38
|
+
}
|
|
39
|
+
else if ('value' in opt) {
|
|
40
|
+
value = opt.value ?? opt.label ?? '';
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
value = opt.label ?? '';
|
|
44
|
+
}
|
|
45
|
+
optionData.push({ value, isSeparator: false });
|
|
46
|
+
selectableIndices.push(index);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Ensure selectedIndex points to a selectable option
|
|
50
|
+
if (!selectableIndices.includes(selectedIndex)) {
|
|
51
|
+
selectedIndex = selectableIndices[0] || 0;
|
|
52
|
+
}
|
|
53
|
+
// Helper function to get next/previous selectable index
|
|
54
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
55
|
+
let nextIndex = currentIndex;
|
|
56
|
+
const maxAttempts = options.length;
|
|
57
|
+
let attempts = 0;
|
|
58
|
+
do {
|
|
59
|
+
if (direction === 'up') {
|
|
60
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
attempts++;
|
|
66
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
67
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
68
|
+
};
|
|
29
69
|
// Render function
|
|
30
70
|
const render = () => {
|
|
31
71
|
(0, terminal_js_1.clearMenu)(state);
|
|
@@ -46,11 +86,18 @@ async function showRadioMenu(config) {
|
|
|
46
86
|
}
|
|
47
87
|
break;
|
|
48
88
|
case 'options':
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
optionData.forEach((item, index) => {
|
|
90
|
+
if (item.isSeparator) {
|
|
91
|
+
// Render section label
|
|
92
|
+
(0, renderer_js_1.renderSectionLabel)(item.label);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Extract number prefix if exists
|
|
96
|
+
const match = item.value.match(/^(\d+)\.\s*/);
|
|
97
|
+
const prefix = match ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
98
|
+
// For radio menus, don't show selection indicator (pass undefined instead of false)
|
|
99
|
+
(0, renderer_js_1.renderOption)(item.value, undefined, index === selectedIndex, prefix);
|
|
100
|
+
}
|
|
54
101
|
lineCount++;
|
|
55
102
|
});
|
|
56
103
|
break;
|
|
@@ -58,13 +105,15 @@ async function showRadioMenu(config) {
|
|
|
58
105
|
if (layout.visible.input) {
|
|
59
106
|
// Calculate display value (current selection number)
|
|
60
107
|
let displayValue = '';
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
const currentItem = optionData[selectedIndex];
|
|
109
|
+
if (currentItem && !currentItem.isSeparator) {
|
|
110
|
+
const match = currentItem.value.match(/^([^.]+)\./);
|
|
111
|
+
if (match) {
|
|
112
|
+
displayValue = match[1];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
116
|
+
}
|
|
68
117
|
}
|
|
69
118
|
(0, renderer_js_1.renderInputPrompt)(prompt, displayValue);
|
|
70
119
|
lineCount++;
|
|
@@ -110,9 +159,19 @@ async function showRadioMenu(config) {
|
|
|
110
159
|
(0, terminal_js_1.clearMenu)(state);
|
|
111
160
|
(0, terminal_js_1.restoreTerminal)(state);
|
|
112
161
|
const selectedOption = options[selectedIndex];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
let value;
|
|
163
|
+
if (typeof selectedOption === 'string') {
|
|
164
|
+
value = selectedOption;
|
|
165
|
+
}
|
|
166
|
+
else if ('type' in selectedOption && selectedOption.type === 'separator') {
|
|
167
|
+
value = '';
|
|
168
|
+
}
|
|
169
|
+
else if ('value' in selectedOption) {
|
|
170
|
+
value = selectedOption.value ?? selectedOption.label ?? '';
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
value = selectedOption.label ?? '';
|
|
174
|
+
}
|
|
116
175
|
resolve({
|
|
117
176
|
index: selectedIndex,
|
|
118
177
|
value
|
|
@@ -121,20 +180,20 @@ async function showRadioMenu(config) {
|
|
|
121
180
|
}
|
|
122
181
|
// Handle arrow keys
|
|
123
182
|
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
124
|
-
selectedIndex = selectedIndex
|
|
183
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
|
|
125
184
|
render();
|
|
126
185
|
return;
|
|
127
186
|
}
|
|
128
187
|
if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
129
|
-
selectedIndex = selectedIndex
|
|
188
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
|
|
130
189
|
render();
|
|
131
190
|
return;
|
|
132
191
|
}
|
|
133
192
|
// Handle number keys
|
|
134
193
|
if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
|
|
135
194
|
const num = parseInt(key, 10);
|
|
136
|
-
if (num > 0 && num <=
|
|
137
|
-
selectedIndex = num - 1;
|
|
195
|
+
if (num > 0 && num <= selectableIndices.length) {
|
|
196
|
+
selectedIndex = selectableIndices[num - 1];
|
|
138
197
|
render();
|
|
139
198
|
}
|
|
140
199
|
return;
|
|
@@ -142,11 +201,14 @@ async function showRadioMenu(config) {
|
|
|
142
201
|
// Handle letter keys
|
|
143
202
|
if (allowLetterKeys && (0, keyboard_js_1.isLetterKey)(key)) {
|
|
144
203
|
const letter = (0, keyboard_js_1.normalizeLetter)(key);
|
|
145
|
-
const index =
|
|
146
|
-
const
|
|
204
|
+
const index = selectableIndices.find(idx => {
|
|
205
|
+
const item = optionData[idx];
|
|
206
|
+
if (item.isSeparator)
|
|
207
|
+
return false;
|
|
208
|
+
const match = item.value.match(/^([a-zA-Z])\./i);
|
|
147
209
|
return match && match[1].toLowerCase() === letter;
|
|
148
210
|
});
|
|
149
|
-
if (index !==
|
|
211
|
+
if (index !== undefined) {
|
|
150
212
|
selectedIndex = index;
|
|
151
213
|
render();
|
|
152
214
|
}
|
package/dist/core/renderer.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export declare function renderHints(hints: string[]): void;
|
|
|
39
39
|
* @param width - Width of separator (default: terminal width)
|
|
40
40
|
*/
|
|
41
41
|
export declare function renderSeparator(char?: string, width?: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Render a section label (menu grouping)
|
|
44
|
+
* @param label - Label text (optional)
|
|
45
|
+
*/
|
|
46
|
+
export declare function renderSectionLabel(label?: string): void;
|
|
42
47
|
/**
|
|
43
48
|
* Render a message with icon
|
|
44
49
|
* @param type - Message type (success, error, warning, info, question)
|
package/dist/core/renderer.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.renderOption = renderOption;
|
|
|
10
10
|
exports.renderInputPrompt = renderInputPrompt;
|
|
11
11
|
exports.renderHints = renderHints;
|
|
12
12
|
exports.renderSeparator = renderSeparator;
|
|
13
|
+
exports.renderSectionLabel = renderSectionLabel;
|
|
13
14
|
exports.renderMessage = renderMessage;
|
|
14
15
|
exports.renderProgress = renderProgress;
|
|
15
16
|
exports.renderBox = renderBox;
|
|
@@ -108,6 +109,18 @@ function renderSeparator(char = '─', width) {
|
|
|
108
109
|
const sepWidth = width || termWidth;
|
|
109
110
|
(0, terminal_js_1.writeLine)(char.repeat(sepWidth));
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Render a section label (menu grouping)
|
|
114
|
+
* @param label - Label text (optional)
|
|
115
|
+
*/
|
|
116
|
+
function renderSectionLabel(label) {
|
|
117
|
+
if (label) {
|
|
118
|
+
(0, terminal_js_1.writeLine)(` ${colors_js_1.colors.dim}────── ${label} ──────${colors_js_1.colors.reset}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
(0, terminal_js_1.writeLine)('');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
111
124
|
/**
|
|
112
125
|
* Render a message with icon
|
|
113
126
|
* @param type - Message type (success, error, warning, info, question)
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { MenuLayout } from './layout.types.js';
|
|
5
5
|
/**
|
|
6
|
-
* Menu option (can be string
|
|
6
|
+
* Menu option (can be string, object with label, or section header)
|
|
7
7
|
*/
|
|
8
8
|
export type MenuOption = string | {
|
|
9
9
|
label: string;
|
|
10
10
|
value?: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'separator';
|
|
13
|
+
label?: string;
|
|
11
14
|
};
|
|
12
15
|
/**
|
|
13
16
|
* Base menu configuration
|
package/package.json
CHANGED