cli-menu-kit 0.1.1 → 0.1.9
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/components/display/header.d.ts +30 -0
- package/dist/components/display/header.js +56 -0
- package/dist/components/display/index.d.ts +1 -0
- package/dist/components/display/index.js +3 -1
- package/dist/components/menus/checkbox-menu.js +103 -26
- package/dist/components/menus/radio-menu.js +119 -29
- package/dist/core/renderer.d.ts +5 -0
- package/dist/core/renderer.js +35 -2
- package/dist/i18n/languages/en.js +3 -2
- package/dist/i18n/languages/zh.js +3 -2
- package/dist/i18n/types.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/types/menu.types.d.ts +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header component for CLI applications
|
|
3
|
+
* Displays ASCII art, title, description, version and URL
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Header configuration
|
|
7
|
+
*/
|
|
8
|
+
export interface HeaderConfig {
|
|
9
|
+
/** ASCII art lines (array of strings) */
|
|
10
|
+
asciiArt?: string[];
|
|
11
|
+
/** Application title */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Application description */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Version number */
|
|
16
|
+
version?: string;
|
|
17
|
+
/** Project URL */
|
|
18
|
+
url?: string;
|
|
19
|
+
/** Optional menu title (e.g., "请选择功能:") */
|
|
20
|
+
menuTitle?: string;
|
|
21
|
+
/** Box width (default: 60) */
|
|
22
|
+
boxWidth?: number;
|
|
23
|
+
/** Header color (default: cyan) */
|
|
24
|
+
color?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render a boxed header with ASCII art, title, and description
|
|
28
|
+
* @param config - Header configuration
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderHeader(config: HeaderConfig): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Header component for CLI applications
|
|
4
|
+
* Displays ASCII art, title, description, version and URL
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.renderHeader = renderHeader;
|
|
8
|
+
const colors_js_1 = require("../../core/colors.js");
|
|
9
|
+
const terminal_js_1 = require("../../core/terminal.js");
|
|
10
|
+
/**
|
|
11
|
+
* Render a boxed header with ASCII art, title, and description
|
|
12
|
+
* @param config - Header configuration
|
|
13
|
+
*/
|
|
14
|
+
function renderHeader(config) {
|
|
15
|
+
const { asciiArt = [], title = '', description = '', version, url, menuTitle, boxWidth = 60, color = colors_js_1.colors.cyan } = config;
|
|
16
|
+
const boldColor = `${color}${colors_js_1.colors.bold}`;
|
|
17
|
+
// Top border
|
|
18
|
+
(0, terminal_js_1.writeLine)('');
|
|
19
|
+
(0, terminal_js_1.writeLine)(`${boldColor}╔${'═'.repeat(boxWidth - 2)}╗${colors_js_1.colors.reset}`);
|
|
20
|
+
// Empty line
|
|
21
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
|
|
22
|
+
// ASCII art (left-aligned with 2 spaces padding)
|
|
23
|
+
if (asciiArt.length > 0) {
|
|
24
|
+
asciiArt.forEach(line => {
|
|
25
|
+
const paddedLine = ` ${line}`.padEnd(boxWidth - 2, ' ');
|
|
26
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${paddedLine}║${colors_js_1.colors.reset}`);
|
|
27
|
+
});
|
|
28
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
|
|
29
|
+
}
|
|
30
|
+
// Title (left-aligned with 2 spaces padding)
|
|
31
|
+
if (title) {
|
|
32
|
+
const paddedTitle = ` ${title}`.padEnd(boxWidth - 2, ' ');
|
|
33
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${paddedTitle}║${colors_js_1.colors.reset}`);
|
|
34
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
|
|
35
|
+
}
|
|
36
|
+
// Description (left-aligned with 2 spaces padding)
|
|
37
|
+
if (description) {
|
|
38
|
+
const paddedDesc = ` ${description}`.padEnd(boxWidth - 2, ' ');
|
|
39
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${paddedDesc}║${colors_js_1.colors.reset}`);
|
|
40
|
+
(0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
|
|
41
|
+
}
|
|
42
|
+
// Bottom border
|
|
43
|
+
(0, terminal_js_1.writeLine)(`${boldColor}╚${'═'.repeat(boxWidth - 2)}╝${colors_js_1.colors.reset}`);
|
|
44
|
+
// Version and URL (outside the box, dimmed)
|
|
45
|
+
if (version || url) {
|
|
46
|
+
const versionText = version ? `Version: ${version}` : '';
|
|
47
|
+
const urlText = url || '';
|
|
48
|
+
const separator = version && url ? ' | ' : '';
|
|
49
|
+
(0, terminal_js_1.writeLine)(`${colors_js_1.colors.dim} ${versionText}${separator}${urlText}${colors_js_1.colors.reset}`);
|
|
50
|
+
}
|
|
51
|
+
(0, terminal_js_1.writeLine)('');
|
|
52
|
+
// Menu title (optional)
|
|
53
|
+
if (menuTitle) {
|
|
54
|
+
(0, terminal_js_1.writeLine)(`${color} ${menuTitle}${colors_js_1.colors.reset}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -6,3 +6,4 @@ export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiH
|
|
|
6
6
|
export { renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator } from './progress.js';
|
|
7
7
|
export { renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage } from './messages.js';
|
|
8
8
|
export { renderSummaryTable, createSummaryTable, createSimpleSummary } from './summary.js';
|
|
9
|
+
export { renderHeader, type HeaderConfig } from './header.js';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Exports all display component functions
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = void 0;
|
|
7
|
+
exports.renderHeader = exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = void 0;
|
|
8
8
|
var headers_js_1 = require("./headers.js");
|
|
9
9
|
Object.defineProperty(exports, "renderSimpleHeader", { enumerable: true, get: function () { return headers_js_1.renderSimpleHeader; } });
|
|
10
10
|
Object.defineProperty(exports, "renderAsciiHeader", { enumerable: true, get: function () { return headers_js_1.renderAsciiHeader; } });
|
|
@@ -29,3 +29,5 @@ var summary_js_1 = require("./summary.js");
|
|
|
29
29
|
Object.defineProperty(exports, "renderSummaryTable", { enumerable: true, get: function () { return summary_js_1.renderSummaryTable; } });
|
|
30
30
|
Object.defineProperty(exports, "createSummaryTable", { enumerable: true, get: function () { return summary_js_1.createSummaryTable; } });
|
|
31
31
|
Object.defineProperty(exports, "createSimpleSummary", { enumerable: true, get: function () { return summary_js_1.createSimpleSummary; } });
|
|
32
|
+
var header_js_1 = require("./header.js");
|
|
33
|
+
Object.defineProperty(exports, "renderHeader", { enumerable: true, get: function () { return header_js_1.renderHeader; } });
|
|
@@ -9,13 +9,32 @@ const layout_types_js_1 = require("../../types/layout.types.js");
|
|
|
9
9
|
const terminal_js_1 = require("../../core/terminal.js");
|
|
10
10
|
const keyboard_js_1 = require("../../core/keyboard.js");
|
|
11
11
|
const renderer_js_1 = require("../../core/renderer.js");
|
|
12
|
+
const registry_js_1 = require("../../i18n/registry.js");
|
|
13
|
+
/**
|
|
14
|
+
* Generate hints based on menu configuration
|
|
15
|
+
*/
|
|
16
|
+
function generateHints(allowSelectAll, allowInvert) {
|
|
17
|
+
const hints = [(0, registry_js_1.t)('hints.arrows'), (0, registry_js_1.t)('hints.space')];
|
|
18
|
+
if (allowSelectAll) {
|
|
19
|
+
hints.push((0, registry_js_1.t)('hints.selectAll'));
|
|
20
|
+
}
|
|
21
|
+
if (allowInvert) {
|
|
22
|
+
hints.push((0, registry_js_1.t)('hints.invert'));
|
|
23
|
+
}
|
|
24
|
+
hints.push((0, registry_js_1.t)('hints.enter'));
|
|
25
|
+
return hints;
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Show a checkbox menu (multi-select)
|
|
14
29
|
* @param config - Menu configuration
|
|
15
30
|
* @returns Promise resolving to selected options
|
|
16
31
|
*/
|
|
17
32
|
async function showCheckboxMenu(config) {
|
|
18
|
-
const { options, title, prompt
|
|
33
|
+
const { options, title, prompt, hints, layout = { ...layout_types_js_1.LAYOUT_PRESETS.SUB_MENU, order: ['input', 'options', 'hints'] }, defaultSelected = [], minSelections = 0, maxSelections, allowSelectAll = true, allowInvert = true, onExit } = config;
|
|
34
|
+
// Use i18n for default prompt if not provided
|
|
35
|
+
const displayPrompt = prompt || (0, registry_js_1.t)('menus.multiSelectPrompt');
|
|
36
|
+
// Generate hints dynamically if not provided
|
|
37
|
+
const displayHints = hints || generateHints(allowSelectAll, allowInvert);
|
|
19
38
|
// Validate options
|
|
20
39
|
if (!options || options.length === 0) {
|
|
21
40
|
throw new Error('CheckboxMenu requires at least one option');
|
|
@@ -24,8 +43,48 @@ async function showCheckboxMenu(config) {
|
|
|
24
43
|
let cursorIndex = 0;
|
|
25
44
|
const selected = new Set(defaultSelected);
|
|
26
45
|
const state = (0, terminal_js_1.initTerminal)();
|
|
27
|
-
//
|
|
28
|
-
const
|
|
46
|
+
// Separate selectable options from separators
|
|
47
|
+
const selectableIndices = [];
|
|
48
|
+
const optionData = [];
|
|
49
|
+
options.forEach((opt, index) => {
|
|
50
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
51
|
+
optionData.push({ value: '', isSeparator: true, label: opt.label });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
let value;
|
|
55
|
+
if (typeof opt === 'string') {
|
|
56
|
+
value = opt;
|
|
57
|
+
}
|
|
58
|
+
else if ('value' in opt) {
|
|
59
|
+
value = opt.value ?? opt.label ?? '';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
value = opt.label ?? '';
|
|
63
|
+
}
|
|
64
|
+
optionData.push({ value, isSeparator: false });
|
|
65
|
+
selectableIndices.push(index);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Ensure cursorIndex points to a selectable option
|
|
69
|
+
if (!selectableIndices.includes(cursorIndex)) {
|
|
70
|
+
cursorIndex = selectableIndices[0] || 0;
|
|
71
|
+
}
|
|
72
|
+
// Helper function to get next/previous selectable index
|
|
73
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
74
|
+
let nextIndex = currentIndex;
|
|
75
|
+
const maxAttempts = options.length;
|
|
76
|
+
let attempts = 0;
|
|
77
|
+
do {
|
|
78
|
+
if (direction === 'up') {
|
|
79
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
attempts++;
|
|
85
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
86
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
87
|
+
};
|
|
29
88
|
// Render function
|
|
30
89
|
const render = () => {
|
|
31
90
|
(0, terminal_js_1.clearMenu)(state);
|
|
@@ -42,20 +101,26 @@ async function showCheckboxMenu(config) {
|
|
|
42
101
|
case 'input':
|
|
43
102
|
if (layout.visible.input) {
|
|
44
103
|
const selectedCount = selected.size;
|
|
45
|
-
const displayValue = `${selectedCount}
|
|
46
|
-
(0, renderer_js_1.renderInputPrompt)(
|
|
104
|
+
const displayValue = `${selectedCount} ${(0, registry_js_1.t)('menus.selectedCount')}`;
|
|
105
|
+
(0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
|
|
47
106
|
lineCount++;
|
|
48
107
|
}
|
|
49
108
|
break;
|
|
50
109
|
case 'options':
|
|
51
|
-
|
|
52
|
-
|
|
110
|
+
optionData.forEach((item, index) => {
|
|
111
|
+
if (item.isSeparator) {
|
|
112
|
+
// Render section label
|
|
113
|
+
(0, renderer_js_1.renderSectionLabel)(item.label);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
(0, renderer_js_1.renderOption)(item.value, selected.has(index), index === cursorIndex);
|
|
117
|
+
}
|
|
53
118
|
lineCount++;
|
|
54
119
|
});
|
|
55
120
|
break;
|
|
56
121
|
case 'hints':
|
|
57
|
-
if (layout.visible.hints &&
|
|
58
|
-
(0, renderer_js_1.renderHints)(
|
|
122
|
+
if (layout.visible.hints && displayHints.length > 0) {
|
|
123
|
+
(0, renderer_js_1.renderHints)(displayHints);
|
|
59
124
|
lineCount++;
|
|
60
125
|
}
|
|
61
126
|
break;
|
|
@@ -100,42 +165,54 @@ async function showCheckboxMenu(config) {
|
|
|
100
165
|
const indices = Array.from(selected).sort((a, b) => a - b);
|
|
101
166
|
const values = indices.map(i => {
|
|
102
167
|
const option = options[i];
|
|
103
|
-
|
|
168
|
+
if (typeof option === 'string') {
|
|
169
|
+
return option;
|
|
170
|
+
}
|
|
171
|
+
else if ('type' in option && option.type === 'separator') {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
else if ('value' in option) {
|
|
175
|
+
return option.value ?? option.label ?? '';
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return option.label ?? '';
|
|
179
|
+
}
|
|
104
180
|
});
|
|
105
181
|
resolve({ indices, values });
|
|
106
182
|
return;
|
|
107
183
|
}
|
|
108
184
|
// Handle Space (toggle selection)
|
|
109
185
|
if ((0, keyboard_js_1.isSpace)(key)) {
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// Check max selections
|
|
115
|
-
if (!maxSelections || selected.size < maxSelections) {
|
|
116
|
-
selected.add(cursorIndex);
|
|
186
|
+
// Only toggle if cursor is on a selectable item
|
|
187
|
+
if (selectableIndices.includes(cursorIndex)) {
|
|
188
|
+
if (selected.has(cursorIndex)) {
|
|
189
|
+
selected.delete(cursorIndex);
|
|
117
190
|
}
|
|
191
|
+
else {
|
|
192
|
+
// Check max selections
|
|
193
|
+
if (!maxSelections || selected.size < maxSelections) {
|
|
194
|
+
selected.add(cursorIndex);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
render();
|
|
118
198
|
}
|
|
119
|
-
render();
|
|
120
199
|
return;
|
|
121
200
|
}
|
|
122
201
|
// Handle arrow keys
|
|
123
202
|
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
124
|
-
cursorIndex = cursorIndex
|
|
203
|
+
cursorIndex = getNextSelectableIndex(cursorIndex, 'up');
|
|
125
204
|
render();
|
|
126
205
|
return;
|
|
127
206
|
}
|
|
128
207
|
if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
129
|
-
cursorIndex = cursorIndex
|
|
208
|
+
cursorIndex = getNextSelectableIndex(cursorIndex, 'down');
|
|
130
209
|
render();
|
|
131
210
|
return;
|
|
132
211
|
}
|
|
133
212
|
// Handle 'A' (select all)
|
|
134
213
|
if (allowSelectAll && (0, keyboard_js_1.normalizeLetter)(key) === 'a') {
|
|
135
|
-
if (!maxSelections || maxSelections >=
|
|
136
|
-
|
|
137
|
-
selected.add(i);
|
|
138
|
-
}
|
|
214
|
+
if (!maxSelections || maxSelections >= selectableIndices.length) {
|
|
215
|
+
selectableIndices.forEach(i => selected.add(i));
|
|
139
216
|
render();
|
|
140
217
|
}
|
|
141
218
|
return;
|
|
@@ -143,13 +220,13 @@ async function showCheckboxMenu(config) {
|
|
|
143
220
|
// Handle 'I' (invert selection)
|
|
144
221
|
if (allowInvert && (0, keyboard_js_1.normalizeLetter)(key) === 'i') {
|
|
145
222
|
const newSelected = new Set();
|
|
146
|
-
|
|
223
|
+
selectableIndices.forEach(i => {
|
|
147
224
|
if (!selected.has(i)) {
|
|
148
225
|
if (!maxSelections || newSelected.size < maxSelections) {
|
|
149
226
|
newSelected.add(i);
|
|
150
227
|
}
|
|
151
228
|
}
|
|
152
|
-
}
|
|
229
|
+
});
|
|
153
230
|
selected.clear();
|
|
154
231
|
newSelected.forEach(i => selected.add(i));
|
|
155
232
|
render();
|
|
@@ -10,13 +10,32 @@ const terminal_js_1 = require("../../core/terminal.js");
|
|
|
10
10
|
const keyboard_js_1 = require("../../core/keyboard.js");
|
|
11
11
|
const renderer_js_1 = require("../../core/renderer.js");
|
|
12
12
|
const colors_js_1 = require("../../core/colors.js");
|
|
13
|
+
const registry_js_1 = require("../../i18n/registry.js");
|
|
14
|
+
/**
|
|
15
|
+
* Generate hints based on menu configuration
|
|
16
|
+
*/
|
|
17
|
+
function generateHints(allowNumberKeys, allowLetterKeys) {
|
|
18
|
+
const hints = [(0, registry_js_1.t)('hints.arrows')];
|
|
19
|
+
if (allowNumberKeys) {
|
|
20
|
+
hints.push((0, registry_js_1.t)('hints.numbers'));
|
|
21
|
+
}
|
|
22
|
+
if (allowLetterKeys) {
|
|
23
|
+
hints.push((0, registry_js_1.t)('hints.letters'));
|
|
24
|
+
}
|
|
25
|
+
hints.push((0, registry_js_1.t)('hints.enter'));
|
|
26
|
+
return hints;
|
|
27
|
+
}
|
|
13
28
|
/**
|
|
14
29
|
* Show a radio menu (single-select)
|
|
15
30
|
* @param config - Menu configuration
|
|
16
31
|
* @returns Promise resolving to selected option
|
|
17
32
|
*/
|
|
18
33
|
async function showRadioMenu(config) {
|
|
19
|
-
const { options, title, prompt
|
|
34
|
+
const { options, title, prompt, hints, layout = layout_types_js_1.LAYOUT_PRESETS.MAIN_MENU, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, onExit } = config;
|
|
35
|
+
// Use i18n for default prompt if not provided
|
|
36
|
+
const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
|
|
37
|
+
// Generate hints dynamically if not provided
|
|
38
|
+
const displayHints = hints || generateHints(allowNumberKeys, allowLetterKeys);
|
|
20
39
|
// Validate options
|
|
21
40
|
if (!options || options.length === 0) {
|
|
22
41
|
throw new Error('RadioMenu requires at least one option');
|
|
@@ -24,8 +43,48 @@ async function showRadioMenu(config) {
|
|
|
24
43
|
// Initialize state
|
|
25
44
|
let selectedIndex = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
26
45
|
const state = (0, terminal_js_1.initTerminal)();
|
|
27
|
-
//
|
|
28
|
-
const
|
|
46
|
+
// Separate selectable options from separators
|
|
47
|
+
const selectableIndices = [];
|
|
48
|
+
const optionData = [];
|
|
49
|
+
options.forEach((opt, index) => {
|
|
50
|
+
if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
|
|
51
|
+
optionData.push({ value: '', isSeparator: true, label: opt.label });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
let value;
|
|
55
|
+
if (typeof opt === 'string') {
|
|
56
|
+
value = opt;
|
|
57
|
+
}
|
|
58
|
+
else if ('value' in opt) {
|
|
59
|
+
value = opt.value ?? opt.label ?? '';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
value = opt.label ?? '';
|
|
63
|
+
}
|
|
64
|
+
optionData.push({ value, isSeparator: false });
|
|
65
|
+
selectableIndices.push(index);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Ensure selectedIndex points to a selectable option
|
|
69
|
+
if (!selectableIndices.includes(selectedIndex)) {
|
|
70
|
+
selectedIndex = selectableIndices[0] || 0;
|
|
71
|
+
}
|
|
72
|
+
// Helper function to get next/previous selectable index
|
|
73
|
+
const getNextSelectableIndex = (currentIndex, direction) => {
|
|
74
|
+
let nextIndex = currentIndex;
|
|
75
|
+
const maxAttempts = options.length;
|
|
76
|
+
let attempts = 0;
|
|
77
|
+
do {
|
|
78
|
+
if (direction === 'up') {
|
|
79
|
+
nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
attempts++;
|
|
85
|
+
} while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
|
|
86
|
+
return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
|
|
87
|
+
};
|
|
29
88
|
// Render function
|
|
30
89
|
const render = () => {
|
|
31
90
|
(0, terminal_js_1.clearMenu)(state);
|
|
@@ -46,12 +105,28 @@ async function showRadioMenu(config) {
|
|
|
46
105
|
}
|
|
47
106
|
break;
|
|
48
107
|
case 'options':
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
108
|
+
optionData.forEach((item, index) => {
|
|
109
|
+
if (item.isSeparator) {
|
|
110
|
+
// Render section label
|
|
111
|
+
(0, renderer_js_1.renderSectionLabel)(item.label);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Check if option starts with a number or letter prefix
|
|
115
|
+
const numberMatch = item.value.match(/^(\d+)\.\s*/);
|
|
116
|
+
const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
|
|
117
|
+
// Don't add prefix if option already has number or letter prefix
|
|
118
|
+
const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
|
|
119
|
+
// Check if this is an Exit option (contains "Exit" or "Quit")
|
|
120
|
+
const isExitOption = /\b(exit|quit)\b/i.test(item.value);
|
|
121
|
+
const displayValue = isExitOption ? `${colors_js_1.colors.red}${item.value}${colors_js_1.colors.reset}` : item.value;
|
|
122
|
+
// For radio menus, don't show selection indicator (pass undefined instead of false)
|
|
123
|
+
(0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
|
|
124
|
+
// Add blank line after last item before next separator
|
|
125
|
+
const nextIndex = index + 1;
|
|
126
|
+
if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
|
|
127
|
+
(0, terminal_js_1.writeLine)('');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
55
130
|
lineCount++;
|
|
56
131
|
});
|
|
57
132
|
break;
|
|
@@ -59,21 +134,23 @@ async function showRadioMenu(config) {
|
|
|
59
134
|
if (layout.visible.input) {
|
|
60
135
|
// Calculate display value (current selection number)
|
|
61
136
|
let displayValue = '';
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
137
|
+
const currentItem = optionData[selectedIndex];
|
|
138
|
+
if (currentItem && !currentItem.isSeparator) {
|
|
139
|
+
const match = currentItem.value.match(/^([^.]+)\./);
|
|
140
|
+
if (match) {
|
|
141
|
+
displayValue = match[1];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
|
|
145
|
+
}
|
|
69
146
|
}
|
|
70
|
-
(0, renderer_js_1.renderInputPrompt)(
|
|
147
|
+
(0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
|
|
71
148
|
lineCount++;
|
|
72
149
|
}
|
|
73
150
|
break;
|
|
74
151
|
case 'hints':
|
|
75
|
-
if (layout.visible.hints &&
|
|
76
|
-
(0, renderer_js_1.renderHints)(
|
|
152
|
+
if (layout.visible.hints && displayHints.length > 0) {
|
|
153
|
+
(0, renderer_js_1.renderHints)(displayHints);
|
|
77
154
|
lineCount++;
|
|
78
155
|
}
|
|
79
156
|
break;
|
|
@@ -111,9 +188,19 @@ async function showRadioMenu(config) {
|
|
|
111
188
|
(0, terminal_js_1.clearMenu)(state);
|
|
112
189
|
(0, terminal_js_1.restoreTerminal)(state);
|
|
113
190
|
const selectedOption = options[selectedIndex];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
191
|
+
let value;
|
|
192
|
+
if (typeof selectedOption === 'string') {
|
|
193
|
+
value = selectedOption;
|
|
194
|
+
}
|
|
195
|
+
else if ('type' in selectedOption && selectedOption.type === 'separator') {
|
|
196
|
+
value = '';
|
|
197
|
+
}
|
|
198
|
+
else if ('value' in selectedOption) {
|
|
199
|
+
value = selectedOption.value ?? selectedOption.label ?? '';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
value = selectedOption.label ?? '';
|
|
203
|
+
}
|
|
117
204
|
resolve({
|
|
118
205
|
index: selectedIndex,
|
|
119
206
|
value
|
|
@@ -122,20 +209,20 @@ async function showRadioMenu(config) {
|
|
|
122
209
|
}
|
|
123
210
|
// Handle arrow keys
|
|
124
211
|
if (key === keyboard_js_1.KEY_CODES.UP) {
|
|
125
|
-
selectedIndex = selectedIndex
|
|
212
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
|
|
126
213
|
render();
|
|
127
214
|
return;
|
|
128
215
|
}
|
|
129
216
|
if (key === keyboard_js_1.KEY_CODES.DOWN) {
|
|
130
|
-
selectedIndex = selectedIndex
|
|
217
|
+
selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
|
|
131
218
|
render();
|
|
132
219
|
return;
|
|
133
220
|
}
|
|
134
221
|
// Handle number keys
|
|
135
222
|
if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
|
|
136
223
|
const num = parseInt(key, 10);
|
|
137
|
-
if (num > 0 && num <=
|
|
138
|
-
selectedIndex = num - 1;
|
|
224
|
+
if (num > 0 && num <= selectableIndices.length) {
|
|
225
|
+
selectedIndex = selectableIndices[num - 1];
|
|
139
226
|
render();
|
|
140
227
|
}
|
|
141
228
|
return;
|
|
@@ -143,11 +230,14 @@ async function showRadioMenu(config) {
|
|
|
143
230
|
// Handle letter keys
|
|
144
231
|
if (allowLetterKeys && (0, keyboard_js_1.isLetterKey)(key)) {
|
|
145
232
|
const letter = (0, keyboard_js_1.normalizeLetter)(key);
|
|
146
|
-
const index =
|
|
147
|
-
const
|
|
233
|
+
const index = selectableIndices.find(idx => {
|
|
234
|
+
const item = optionData[idx];
|
|
235
|
+
if (item.isSeparator)
|
|
236
|
+
return false;
|
|
237
|
+
const match = item.value.match(/^([a-zA-Z])\./i);
|
|
148
238
|
return match && match[1].toLowerCase() === letter;
|
|
149
239
|
});
|
|
150
|
-
if (index !==
|
|
240
|
+
if (index !== undefined) {
|
|
151
241
|
selectedIndex = index;
|
|
152
242
|
render();
|
|
153
243
|
}
|
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;
|
|
@@ -79,7 +80,7 @@ function renderOption(text, isSelected, isHighlighted, prefix) {
|
|
|
79
80
|
* @param showCursor - Whether to show cursor
|
|
80
81
|
*/
|
|
81
82
|
function renderInputPrompt(prompt, value, showCursor = false) {
|
|
82
|
-
let line = ` ${
|
|
83
|
+
let line = ` ${prompt} `;
|
|
83
84
|
if (value) {
|
|
84
85
|
line += `${colors_js_1.colors.cyan}${value}${colors_js_1.colors.reset}`;
|
|
85
86
|
}
|
|
@@ -95,7 +96,19 @@ function renderInputPrompt(prompt, value, showCursor = false) {
|
|
|
95
96
|
function renderHints(hints) {
|
|
96
97
|
if (hints.length === 0)
|
|
97
98
|
return;
|
|
98
|
-
|
|
99
|
+
// Format each hint: symbols/numbers in black, text in gray
|
|
100
|
+
const formattedHints = hints.map(hint => {
|
|
101
|
+
const parts = hint.split(' ');
|
|
102
|
+
if (parts.length >= 2) {
|
|
103
|
+
// First part (symbols/numbers) in normal color, rest in dim
|
|
104
|
+
const symbols = parts[0];
|
|
105
|
+
const text = parts.slice(1).join(' ');
|
|
106
|
+
return `${symbols} ${colors_js_1.colors.dim}${text}${colors_js_1.colors.reset}`;
|
|
107
|
+
}
|
|
108
|
+
// If no space, keep entire hint in dim
|
|
109
|
+
return `${colors_js_1.colors.dim}${hint}${colors_js_1.colors.reset}`;
|
|
110
|
+
});
|
|
111
|
+
const hintLine = ` ${formattedHints.join(' • ')}`;
|
|
99
112
|
(0, terminal_js_1.writeLine)(hintLine);
|
|
100
113
|
}
|
|
101
114
|
/**
|
|
@@ -108,6 +121,26 @@ function renderSeparator(char = '─', width) {
|
|
|
108
121
|
const sepWidth = width || termWidth;
|
|
109
122
|
(0, terminal_js_1.writeLine)(char.repeat(sepWidth));
|
|
110
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Render a section label (menu grouping)
|
|
126
|
+
* @param label - Label text (optional)
|
|
127
|
+
*/
|
|
128
|
+
function renderSectionLabel(label) {
|
|
129
|
+
if (label) {
|
|
130
|
+
const totalWidth = 50; // Fixed total width for consistency
|
|
131
|
+
const padding = 2; // Spaces around label
|
|
132
|
+
const labelWithPadding = ` ${label} `;
|
|
133
|
+
const labelLength = labelWithPadding.length;
|
|
134
|
+
const dashesTotal = totalWidth - labelLength;
|
|
135
|
+
const dashesLeft = Math.floor(dashesTotal / 2);
|
|
136
|
+
const dashesRight = dashesTotal - dashesLeft;
|
|
137
|
+
const line = ` ${colors_js_1.colors.dim}${'─'.repeat(dashesLeft)}${labelWithPadding}${'─'.repeat(dashesRight)}${colors_js_1.colors.reset}`;
|
|
138
|
+
(0, terminal_js_1.writeLine)(line);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
(0, terminal_js_1.writeLine)('');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
111
144
|
/**
|
|
112
145
|
* Render a message with icon
|
|
113
146
|
* @param type - Message type (success, error, warning, info, question)
|
|
@@ -16,10 +16,11 @@ exports.en = {
|
|
|
16
16
|
space: 'Space Toggle',
|
|
17
17
|
enter: '⏎ Confirm',
|
|
18
18
|
numbers: '0-9 Number keys',
|
|
19
|
-
letters: '
|
|
19
|
+
letters: 'Letter Shortcuts',
|
|
20
20
|
selectAll: 'A Select all',
|
|
21
21
|
invert: 'I Invert',
|
|
22
|
-
yesNo: 'Y/N Quick keys'
|
|
22
|
+
yesNo: 'Y/N Quick keys',
|
|
23
|
+
exit: 'Ctrl+C Exit'
|
|
23
24
|
},
|
|
24
25
|
messages: {
|
|
25
26
|
success: 'Success',
|
|
@@ -16,10 +16,11 @@ exports.zh = {
|
|
|
16
16
|
space: '空格 选中/取消',
|
|
17
17
|
enter: '⏎ 确认',
|
|
18
18
|
numbers: '0-9 输入序号',
|
|
19
|
-
letters: '
|
|
19
|
+
letters: '字母 快捷键',
|
|
20
20
|
selectAll: 'A 全选',
|
|
21
21
|
invert: 'I 反选',
|
|
22
|
-
yesNo: 'Y/N 快捷键'
|
|
22
|
+
yesNo: 'Y/N 快捷键',
|
|
23
|
+
exit: 'Ctrl+C 退出'
|
|
23
24
|
},
|
|
24
25
|
messages: {
|
|
25
26
|
success: '成功',
|
package/dist/i18n/types.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { menuAPI as menu, inputAPI as input, wizardAPI as wizard } from './api.j
|
|
|
6
6
|
export { default } from './api.js';
|
|
7
7
|
export { showRadioMenu, showCheckboxMenu, showBooleanMenu } from './components/menus/index.js';
|
|
8
8
|
export { showTextInput, showNumberInput, showLanguageSelector, showModifyField } from './components/inputs/index.js';
|
|
9
|
-
export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiHeader, renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator, renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage, renderSummaryTable, createSummaryTable, createSimpleSummary } from './components/display/index.js';
|
|
9
|
+
export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiHeader, renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator, renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage, renderSummaryTable, createSummaryTable, createSimpleSummary, renderHeader, type HeaderConfig } from './components/display/index.js';
|
|
10
10
|
export { runWizard, createWizard, WizardConfig, WizardStep, WizardResult } from './features/wizard.js';
|
|
11
11
|
export { registerCommand, unregisterCommand, clearCustomCommands, isCommand, parseCommand, handleCommand, getAvailableCommands, showCommandHelp } from './features/commands.js';
|
|
12
12
|
export { getCurrentLanguage, setLanguage, t, registerLanguage, getAvailableLanguages, getCurrentLanguageMap } from './i18n/registry.js';
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.
|
|
25
|
-
exports.LAYOUT_PRESETS = exports.KEY_CODES = exports.colorize = void 0;
|
|
24
|
+
exports.createGradient = exports.colors = exports.getCurrentLanguageMap = exports.getAvailableLanguages = exports.registerLanguage = exports.t = exports.setLanguage = exports.getCurrentLanguage = exports.showCommandHelp = exports.getAvailableCommands = exports.handleCommand = exports.parseCommand = exports.isCommand = exports.clearCustomCommands = exports.unregisterCommand = exports.registerCommand = exports.createWizard = exports.runWizard = exports.renderHeader = exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = exports.showModifyField = exports.showLanguageSelector = exports.showNumberInput = exports.showTextInput = exports.showBooleanMenu = exports.showCheckboxMenu = exports.showRadioMenu = exports.default = exports.wizard = exports.input = exports.menu = void 0;
|
|
25
|
+
exports.LAYOUT_PRESETS = exports.KEY_CODES = exports.colorize = exports.applyGradient = void 0;
|
|
26
26
|
// Export unified API
|
|
27
27
|
var api_js_1 = require("./api.js");
|
|
28
28
|
Object.defineProperty(exports, "menu", { enumerable: true, get: function () { return api_js_1.menuAPI; } });
|
|
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "createMessage", { enumerable: true, get: functio
|
|
|
63
63
|
Object.defineProperty(exports, "renderSummaryTable", { enumerable: true, get: function () { return index_js_3.renderSummaryTable; } });
|
|
64
64
|
Object.defineProperty(exports, "createSummaryTable", { enumerable: true, get: function () { return index_js_3.createSummaryTable; } });
|
|
65
65
|
Object.defineProperty(exports, "createSimpleSummary", { enumerable: true, get: function () { return index_js_3.createSimpleSummary; } });
|
|
66
|
+
Object.defineProperty(exports, "renderHeader", { enumerable: true, get: function () { return index_js_3.renderHeader; } });
|
|
66
67
|
// Export features
|
|
67
68
|
var wizard_js_1 = require("./features/wizard.js");
|
|
68
69
|
Object.defineProperty(exports, "runWizard", { enumerable: true, get: function () { return wizard_js_1.runWizard; } });
|
|
@@ -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