cli-menu-kit 0.1.1 → 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,12 +86,18 @@ async function showRadioMenu(config) {
|
|
|
46
86
|
}
|
|
47
87
|
break;
|
|
48
88
|
case 'options':
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
55
101
|
lineCount++;
|
|
56
102
|
});
|
|
57
103
|
break;
|
|
@@ -59,13 +105,15 @@ async function showRadioMenu(config) {
|
|
|
59
105
|
if (layout.visible.input) {
|
|
60
106
|
// Calculate display value (current selection number)
|
|
61
107
|
let displayValue = '';
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
117
|
}
|
|
70
118
|
(0, renderer_js_1.renderInputPrompt)(prompt, displayValue);
|
|
71
119
|
lineCount++;
|
|
@@ -111,9 +159,19 @@ async function showRadioMenu(config) {
|
|
|
111
159
|
(0, terminal_js_1.clearMenu)(state);
|
|
112
160
|
(0, terminal_js_1.restoreTerminal)(state);
|
|
113
161
|
const selectedOption = options[selectedIndex];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
175
|
resolve({
|
|
118
176
|
index: selectedIndex,
|
|
119
177
|
value
|
|
@@ -122,20 +180,20 @@ async function showRadioMenu(config) {
|
|
|
122
180
|
}
|
|
123
181
|
// Handle arrow keys
|
|
124
182
|
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
125
|
-
selectedIndex = selectedIndex
|
|
183
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
|
|
126
184
|
render();
|
|
127
185
|
return;
|
|
128
186
|
}
|
|
129
187
|
if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
130
|
-
selectedIndex = selectedIndex
|
|
188
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
|
|
131
189
|
render();
|
|
132
190
|
return;
|
|
133
191
|
}
|
|
134
192
|
// Handle number keys
|
|
135
193
|
if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
|
|
136
194
|
const num = parseInt(key, 10);
|
|
137
|
-
if (num > 0 && num <=
|
|
138
|
-
selectedIndex = num - 1;
|
|
195
|
+
if (num > 0 && num <= selectableIndices.length) {
|
|
196
|
+
selectedIndex = selectableIndices[num - 1];
|
|
139
197
|
render();
|
|
140
198
|
}
|
|
141
199
|
return;
|
|
@@ -143,11 +201,14 @@ async function showRadioMenu(config) {
|
|
|
143
201
|
// Handle letter keys
|
|
144
202
|
if (allowLetterKeys && (0, keyboard_js_1.isLetterKey)(key)) {
|
|
145
203
|
const letter = (0, keyboard_js_1.normalizeLetter)(key);
|
|
146
|
-
const index =
|
|
147
|
-
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);
|
|
148
209
|
return match && match[1].toLowerCase() === letter;
|
|
149
210
|
});
|
|
150
|
-
if (index !==
|
|
211
|
+
if (index !== undefined) {
|
|
151
212
|
selectedIndex = index;
|
|
152
213
|
render();
|
|
153
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