cli-menu-kit 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/components/display/header.d.ts +40 -0
  2. package/dist/components/display/header.js +331 -18
  3. package/dist/components/display/headers.d.ts +1 -0
  4. package/dist/components/display/headers.js +15 -5
  5. package/dist/components/display/index.d.ts +1 -1
  6. package/dist/components/display/index.js +3 -1
  7. package/dist/components/display/messages.js +5 -5
  8. package/dist/components/display/progress.d.ts +17 -0
  9. package/dist/components/display/progress.js +18 -0
  10. package/dist/components/display/summary.js +72 -10
  11. package/dist/components/display/table.d.ts +2 -0
  12. package/dist/components/display/table.js +7 -6
  13. package/dist/components/inputs/language-input.js +8 -5
  14. package/dist/components/inputs/number-input.js +19 -14
  15. package/dist/components/inputs/text-input.js +50 -13
  16. package/dist/components/menus/boolean-menu.js +33 -20
  17. package/dist/components/menus/checkbox-menu.js +5 -2
  18. package/dist/components/menus/checkbox-table-menu.js +12 -9
  19. package/dist/components/menus/radio-menu-split.d.ts +1 -0
  20. package/dist/components/menus/radio-menu-split.js +26 -16
  21. package/dist/components/menus/radio-menu.js +67 -38
  22. package/dist/components.js +3 -3
  23. package/dist/config/index.d.ts +5 -0
  24. package/dist/config/index.js +21 -0
  25. package/dist/config/language-config.d.ts +73 -0
  26. package/dist/config/language-config.js +157 -0
  27. package/dist/config/user-config.d.ts +83 -0
  28. package/dist/config/user-config.js +185 -0
  29. package/dist/core/colors.d.ts +24 -18
  30. package/dist/core/colors.js +74 -7
  31. package/dist/core/renderer.js +26 -18
  32. package/dist/core/terminal.d.ts +13 -0
  33. package/dist/core/terminal.js +87 -0
  34. package/dist/features/commands.js +23 -22
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.js +21 -2
  37. package/dist/layout.d.ts +50 -51
  38. package/dist/layout.js +69 -117
  39. package/dist/page-layout.d.ts +31 -0
  40. package/dist/page-layout.js +46 -7
  41. package/dist/types/input.types.d.ts +8 -0
  42. package/dist/types/layout.types.d.ts +56 -0
  43. package/dist/types/layout.types.js +36 -0
  44. package/dist/types/menu.types.d.ts +4 -0
  45. package/package.json +4 -1
@@ -10,6 +10,58 @@ exports.createSimpleSummary = createSimpleSummary;
10
10
  const terminal_js_1 = require("../../core/terminal.js");
11
11
  const colors_js_1 = require("../../core/colors.js");
12
12
  const terminal_js_2 = require("../../core/terminal.js");
13
+ function resolveColorSpec(spec) {
14
+ if (!spec) {
15
+ return '';
16
+ }
17
+ return spec
18
+ .split('+')
19
+ .map(part => {
20
+ const token = part.trim();
21
+ if (!token) {
22
+ return '';
23
+ }
24
+ const named = colors_js_1.colors[token];
25
+ if (named) {
26
+ return named;
27
+ }
28
+ // Accept raw ANSI sequences (e.g. values from getUIColors()).
29
+ if (/^\x1b\[[0-9;]*m$/.test(token)) {
30
+ return token;
31
+ }
32
+ return '';
33
+ })
34
+ .join('');
35
+ }
36
+ function splitLongToken(token, maxWidth) {
37
+ if (token.length <= maxWidth) {
38
+ return [token];
39
+ }
40
+ const chunks = [];
41
+ let rest = token;
42
+ const breakChars = ['/', '\\', '-', '_', '.'];
43
+ while (rest.length > maxWidth) {
44
+ let breakPos = -1;
45
+ for (const ch of breakChars) {
46
+ const idx = rest.lastIndexOf(ch, maxWidth - 1);
47
+ if (idx > breakPos) {
48
+ breakPos = idx;
49
+ }
50
+ }
51
+ if (breakPos <= 0) {
52
+ breakPos = maxWidth;
53
+ }
54
+ else {
55
+ breakPos += 1;
56
+ }
57
+ chunks.push(rest.slice(0, breakPos));
58
+ rest = rest.slice(breakPos);
59
+ }
60
+ if (rest.length > 0) {
61
+ chunks.push(rest);
62
+ }
63
+ return chunks;
64
+ }
13
65
  /**
14
66
  * Wrap text to fit within a specific width, preserving ANSI color codes
15
67
  */
@@ -17,10 +69,15 @@ function wrapTextWithColors(text, maxWidth) {
17
69
  // Extract ANSI codes and plain text
18
70
  const ansiRegex = /\x1b\[[0-9;]*m/g;
19
71
  const plainText = text.replace(ansiRegex, '');
72
+ const hasAnsi = /\x1b\[[0-9;]*m/.test(text);
20
73
  // If plain text fits, return as-is
21
74
  if (plainText.length <= maxWidth) {
22
75
  return [text];
23
76
  }
77
+ // Fast path for plain text (no ANSI): support long token wrapping (e.g. file paths).
78
+ if (!hasAnsi) {
79
+ return wrapText(text, maxWidth);
80
+ }
24
81
  // Find all ANSI codes and their positions in the original text
25
82
  const codes = [];
26
83
  let match;
@@ -94,6 +151,16 @@ function wrapText(text, maxWidth) {
94
151
  const lines = [];
95
152
  let currentLine = '';
96
153
  for (const word of words) {
154
+ if (word.length > maxWidth) {
155
+ if (currentLine) {
156
+ lines.push(currentLine);
157
+ currentLine = '';
158
+ }
159
+ const chunks = splitLongToken(word, maxWidth);
160
+ lines.push(...chunks.slice(0, -1));
161
+ currentLine = chunks[chunks.length - 1] || '';
162
+ continue;
163
+ }
97
164
  const testLine = currentLine ? `${currentLine} ${word}` : word;
98
165
  if (testLine.length <= maxWidth) {
99
166
  currentLine = testLine;
@@ -136,12 +203,7 @@ function renderSummaryTable(config) {
136
203
  (0, terminal_js_1.writeLine)(`│${' '.repeat(boxWidth - 2)}│`);
137
204
  // Title if provided
138
205
  if (title) {
139
- // Parse color configuration (supports "color" or "color+style" format)
140
- let titleColor = '';
141
- if (finalColors.title) {
142
- const parts = finalColors.title.split('+');
143
- titleColor = parts.map(part => colors_js_1.colors[part.trim()] || '').join('');
144
- }
206
+ const titleColor = resolveColorSpec(finalColors.title);
145
207
  const resetColor = titleColor ? colors_js_1.colors.reset : '';
146
208
  let titleLine;
147
209
  let remainingSpace;
@@ -173,7 +235,7 @@ function renderSummaryTable(config) {
173
235
  sections.forEach((section, sectionIndex) => {
174
236
  // Section header if provided
175
237
  if (section.header) {
176
- const headerColor = finalColors.sectionHeader ? colors_js_1.colors[finalColors.sectionHeader] || '' : '';
238
+ const headerColor = resolveColorSpec(finalColors.sectionHeader);
177
239
  const resetColor = headerColor ? colors_js_1.colors.reset : '';
178
240
  const headerLine = ` ${headerColor}${section.header}${resetColor}`;
179
241
  const remainingSpace = boxWidth - section.header.length - 4;
@@ -188,7 +250,7 @@ function renderSummaryTable(config) {
188
250
  // Wrap the value (preserving colors if present)
189
251
  const wrappedLines = wrapTextWithColors(item.value, valueMaxWidth);
190
252
  // First line with key
191
- const keyColor = finalColors.key ? colors_js_1.colors[finalColors.key] || '' : '';
253
+ const keyColor = resolveColorSpec(finalColors.key);
192
254
  const keyResetColor = keyColor ? colors_js_1.colors.reset : '';
193
255
  // wrappedLines already contain colors, don't add valueColor
194
256
  const firstLine = ` ${keyColor}${item.key}:${keyResetColor}${' '.repeat(Math.max(1, keyPadding - item.key.length))}${wrappedLines[0]}`;
@@ -205,9 +267,9 @@ function renderSummaryTable(config) {
205
267
  }
206
268
  else {
207
269
  // No wrapping needed
208
- const keyColor = finalColors.key ? colors_js_1.colors[finalColors.key] || '' : '';
270
+ const keyColor = resolveColorSpec(finalColors.key);
209
271
  const keyResetColor = keyColor ? colors_js_1.colors.reset : '';
210
- const valueColor = finalColors.value ? colors_js_1.colors[finalColors.value] || '' : '';
272
+ const valueColor = resolveColorSpec(finalColors.value);
211
273
  const valueResetColor = valueColor ? colors_js_1.colors.reset : '';
212
274
  // Only wrap value with color if valueColor is set, otherwise preserve original colors in item.value
213
275
  const coloredValue = valueColor ? `${valueColor}${item.value}${valueResetColor}` : item.value;
@@ -27,6 +27,8 @@ export interface TableConfig {
27
27
  showBorders?: boolean;
28
28
  /** Show header separator */
29
29
  showHeaderSeparator?: boolean;
30
+ /** Border color (ANSI escape code, e.g., '\x1b[2m' for dim/gray) */
31
+ borderColor?: string;
30
32
  }
31
33
  /**
32
34
  * Render table component
@@ -12,10 +12,11 @@ const terminal_js_1 = require("../../core/terminal.js");
12
12
  * @param config - Table configuration
13
13
  */
14
14
  function renderTable(config) {
15
- const { columns, data, showBorders = true, showHeaderSeparator = true } = config;
15
+ const { columns, data, showBorders = true, showHeaderSeparator = true, borderColor = '' } = config;
16
16
  if (columns.length === 0 || data.length === 0) {
17
17
  return;
18
18
  }
19
+ const resetColor = borderColor ? '\x1b[0m' : '';
19
20
  // Calculate column widths
20
21
  const columnWidths = columns.map((col, index) => {
21
22
  if (col.width)
@@ -28,7 +29,7 @@ function renderTable(config) {
28
29
  // Render top border
29
30
  if (showBorders) {
30
31
  const border = columnWidths.map(w => '─'.repeat(w)).join('┬');
31
- (0, terminal_js_1.writeLine)(`┌${border}┐`);
32
+ (0, terminal_js_1.writeLine)(`${borderColor}┌${border}┐${resetColor}`);
32
33
  }
33
34
  // Render header
34
35
  const headerCells = columns.map((col, index) => {
@@ -37,7 +38,7 @@ function renderTable(config) {
37
38
  return alignText(col.header, width, align);
38
39
  });
39
40
  if (showBorders) {
40
- (0, terminal_js_1.writeLine)(`│${headerCells.join('│')}│`);
41
+ (0, terminal_js_1.writeLine)(`${borderColor}│${resetColor}${headerCells.join(`${borderColor}│${resetColor}`)}${borderColor}│${resetColor}`);
41
42
  }
42
43
  else {
43
44
  (0, terminal_js_1.writeLine)(headerCells.join(' '));
@@ -46,7 +47,7 @@ function renderTable(config) {
46
47
  if (showHeaderSeparator) {
47
48
  if (showBorders) {
48
49
  const separator = columnWidths.map(w => '─'.repeat(w)).join('┼');
49
- (0, terminal_js_1.writeLine)(`├${separator}┤`);
50
+ (0, terminal_js_1.writeLine)(`${borderColor}├${separator}┤${resetColor}`);
50
51
  }
51
52
  else {
52
53
  const separator = columnWidths.map(w => '─'.repeat(w)).join(' ');
@@ -62,7 +63,7 @@ function renderTable(config) {
62
63
  return alignText(value, width, align);
63
64
  });
64
65
  if (showBorders) {
65
- (0, terminal_js_1.writeLine)(`│${cells.join('│')}│`);
66
+ (0, terminal_js_1.writeLine)(`${borderColor}│${resetColor}${cells.join(`${borderColor}│${resetColor}`)}${borderColor}│${resetColor}`);
66
67
  }
67
68
  else {
68
69
  (0, terminal_js_1.writeLine)(cells.join(' '));
@@ -71,7 +72,7 @@ function renderTable(config) {
71
72
  // Render bottom border
72
73
  if (showBorders) {
73
74
  const border = columnWidths.map(w => '─'.repeat(w)).join('┴');
74
- (0, terminal_js_1.writeLine)(`└${border}┘`);
75
+ (0, terminal_js_1.writeLine)(`${borderColor}└${border}┘${resetColor}`);
75
76
  }
76
77
  }
77
78
  /**
@@ -9,13 +9,14 @@ const terminal_js_1 = require("../../core/terminal.js");
9
9
  const keyboard_js_1 = require("../../core/keyboard.js");
10
10
  const renderer_js_1 = require("../../core/renderer.js");
11
11
  const colors_js_1 = require("../../core/colors.js");
12
+ const registry_js_1 = require("../../i18n/registry.js");
12
13
  /**
13
14
  * Show a language selector
14
15
  * @param config - Selector configuration
15
16
  * @returns Promise resolving to selected language code
16
17
  */
17
18
  async function showLanguageSelector(config) {
18
- const { languages, defaultLanguage, prompt = '选择语言 / Select Language', onExit } = config;
19
+ const { languages, defaultLanguage, prompt = 'Select Language', onExit, preserveOnExit = false } = config;
19
20
  // Validate languages
20
21
  if (!languages || languages.length === 0) {
21
22
  throw new Error('LanguageSelector requires at least one language');
@@ -33,7 +34,7 @@ async function showLanguageSelector(config) {
33
34
  (0, terminal_js_1.clearMenu)(state);
34
35
  let lineCount = 0;
35
36
  // Render header
36
- (0, renderer_js_1.renderHeader)(` ${prompt}`, colors_js_1.colors.cyan);
37
+ (0, renderer_js_1.renderHeader)(` ${prompt}`, colors_js_1.uiColors.primary);
37
38
  lineCount++;
38
39
  (0, renderer_js_1.renderBlankLines)(1);
39
40
  lineCount++;
@@ -48,7 +49,7 @@ async function showLanguageSelector(config) {
48
49
  // Render hints
49
50
  (0, renderer_js_1.renderBlankLines)(1);
50
51
  lineCount++;
51
- (0, renderer_js_1.renderHints)(['↑↓ 方向键', '1-9 输入序号', '⏎ 确认']);
52
+ (0, renderer_js_1.renderHints)([(0, registry_js_1.t)('hints.arrows'), (0, registry_js_1.t)('hints.numbers'), (0, registry_js_1.t)('hints.enter')]);
52
53
  lineCount++;
53
54
  state.renderedLines = lineCount;
54
55
  };
@@ -60,13 +61,15 @@ async function showLanguageSelector(config) {
60
61
  // Handle Ctrl+C
61
62
  if ((0, keyboard_js_1.isCtrlC)(key)) {
62
63
  state.stdin.removeListener('data', onData);
63
- (0, terminal_js_1.clearMenu)(state);
64
+ if (!preserveOnExit) {
65
+ (0, terminal_js_1.clearMenu)(state);
66
+ }
64
67
  (0, terminal_js_1.restoreTerminal)(state);
65
68
  if (onExit) {
66
69
  onExit();
67
70
  }
68
71
  else {
69
- console.log('\n👋 再见!');
72
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
70
73
  }
71
74
  process.exit(0);
72
75
  }
@@ -8,13 +8,15 @@ exports.showNumberInput = showNumberInput;
8
8
  const terminal_js_1 = require("../../core/terminal.js");
9
9
  const keyboard_js_1 = require("../../core/keyboard.js");
10
10
  const colors_js_1 = require("../../core/colors.js");
11
+ const registry_js_1 = require("../../i18n/registry.js");
11
12
  /**
12
13
  * Show a number input prompt
13
14
  * @param config - Input configuration
14
15
  * @returns Promise resolving to entered number
15
16
  */
16
17
  async function showNumberInput(config) {
17
- const { prompt, defaultValue, min, max, allowDecimals = false, allowNegative = false, validate, errorMessage, onExit } = config;
18
+ const { lang: langInput, prompt, defaultValue, min, max, allowDecimals = false, allowNegative = false, validate, errorMessage, onExit, preserveOnExit = false } = config;
19
+ const lang = langInput ?? (0, registry_js_1.getCurrentLanguage)();
18
20
  const state = (0, terminal_js_1.initTerminal)();
19
21
  let inputValue = '';
20
22
  let errorMsg = '';
@@ -26,20 +28,21 @@ async function showNumberInput(config) {
26
28
  if (min !== undefined || max !== undefined) {
27
29
  const constraints = [];
28
30
  if (min !== undefined)
29
- constraints.push(`最小: ${min}`);
31
+ constraints.push(`${(0, registry_js_1.t)('inputs.minValue')}: ${min}`);
30
32
  if (max !== undefined)
31
- constraints.push(`最大: ${max}`);
32
- promptLine += ` ${colors_js_1.colors.dim}(${constraints.join(', ')})${colors_js_1.colors.reset}`;
33
+ constraints.push(`${(0, registry_js_1.t)('inputs.maxValue')}: ${max}`);
34
+ promptLine += ` ${colors_js_1.uiColors.textSecondary}(${constraints.join(', ')})${colors_js_1.colors.reset}`;
33
35
  }
34
36
  if (defaultValue !== undefined && !inputValue) {
35
- promptLine += ` ${colors_js_1.colors.dim}(默认: ${defaultValue})${colors_js_1.colors.reset}`;
37
+ const defaultLabel = lang === 'en' ? 'default' : (0, registry_js_1.t)('inputs.defaultValue');
38
+ promptLine += ` ${colors_js_1.uiColors.textSecondary}(${defaultLabel}: ${defaultValue})${colors_js_1.colors.reset}`;
36
39
  }
37
- promptLine += `: ${colors_js_1.colors.cyan}${inputValue}_${colors_js_1.colors.reset}`;
40
+ promptLine += `: ${colors_js_1.uiColors.primary}${inputValue}_${colors_js_1.colors.reset}`;
38
41
  (0, terminal_js_1.writeLine)(promptLine);
39
42
  lineCount++;
40
43
  // Render error message if any
41
44
  if (errorMsg) {
42
- (0, terminal_js_1.writeLine)(` ${colors_js_1.colors.red}✗ ${errorMsg}${colors_js_1.colors.reset}`);
45
+ (0, terminal_js_1.writeLine)(` ${colors_js_1.uiColors.error}✗ ${errorMsg}${colors_js_1.colors.reset}`);
43
46
  lineCount++;
44
47
  }
45
48
  state.renderedLines = lineCount;
@@ -52,13 +55,15 @@ async function showNumberInput(config) {
52
55
  // Handle Ctrl+C
53
56
  if ((0, keyboard_js_1.isCtrlC)(key)) {
54
57
  state.stdin.removeListener('data', onData);
55
- (0, terminal_js_1.clearMenu)(state);
58
+ if (!preserveOnExit) {
59
+ (0, terminal_js_1.clearMenu)(state);
60
+ }
56
61
  (0, terminal_js_1.restoreTerminal)(state);
57
62
  if (onExit) {
58
63
  onExit();
59
64
  }
60
65
  else {
61
- console.log('\n👋 再见!');
66
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
62
67
  }
63
68
  process.exit(0);
64
69
  }
@@ -67,7 +72,7 @@ async function showNumberInput(config) {
67
72
  const finalValue = inputValue || (defaultValue !== undefined ? String(defaultValue) : '');
68
73
  // Check if empty
69
74
  if (!finalValue) {
70
- errorMsg = errorMessage || '请输入数字';
75
+ errorMsg = errorMessage || (0, registry_js_1.t)('inputs.enterNumber');
71
76
  render();
72
77
  return;
73
78
  }
@@ -75,19 +80,19 @@ async function showNumberInput(config) {
75
80
  const num = allowDecimals ? parseFloat(finalValue) : parseInt(finalValue, 10);
76
81
  // Check if valid number
77
82
  if (isNaN(num)) {
78
- errorMsg = errorMessage || '输入必须是有效的数字';
83
+ errorMsg = errorMessage || (0, registry_js_1.t)('inputs.invalidInput');
79
84
  render();
80
85
  return;
81
86
  }
82
87
  // Check min
83
88
  if (min !== undefined && num < min) {
84
- errorMsg = errorMessage || `数字不能小于 ${min}`;
89
+ errorMsg = errorMessage || `${(0, registry_js_1.t)('inputs.minValue')}: ${min}`;
85
90
  render();
86
91
  return;
87
92
  }
88
93
  // Check max
89
94
  if (max !== undefined && num > max) {
90
- errorMsg = errorMessage || `数字不能大于 ${max}`;
95
+ errorMsg = errorMessage || `${(0, registry_js_1.t)('inputs.maxValue')}: ${max}`;
91
96
  render();
92
97
  return;
93
98
  }
@@ -95,7 +100,7 @@ async function showNumberInput(config) {
95
100
  if (validate) {
96
101
  const validationResult = validate(String(num));
97
102
  if (validationResult !== true) {
98
- errorMsg = typeof validationResult === 'string' ? validationResult : (errorMessage || '输入无效');
103
+ errorMsg = typeof validationResult === 'string' ? validationResult : (errorMessage || (0, registry_js_1.t)('inputs.invalidInput'));
99
104
  render();
100
105
  return;
101
106
  }
@@ -8,13 +8,36 @@ exports.showTextInput = showTextInput;
8
8
  const terminal_js_1 = require("../../core/terminal.js");
9
9
  const keyboard_js_1 = require("../../core/keyboard.js");
10
10
  const colors_js_1 = require("../../core/colors.js");
11
+ const registry_js_1 = require("../../i18n/registry.js");
12
+ /**
13
+ * Normalize a terminal data chunk to printable text.
14
+ * Supports bracketed paste and ignores control/escape sequences.
15
+ */
16
+ function normalizeTextChunk(raw) {
17
+ if (!raw) {
18
+ return '';
19
+ }
20
+ let chunk = raw;
21
+ // Bracketed paste wrappers used by many terminals.
22
+ chunk = chunk.replace(/\x1b\[200~/g, '');
23
+ chunk = chunk.replace(/\x1b\[201~/g, '');
24
+ // Remove CSI escape sequences (arrows, function keys, etc.).
25
+ chunk = chunk.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '');
26
+ // Remove remaining ESC-prefixed control bytes.
27
+ chunk = chunk.replace(/\x1b./g, '');
28
+ // Single-line input: strip CR/LF and other control chars.
29
+ chunk = chunk.replace(/[\r\n]/g, '');
30
+ chunk = chunk.replace(/[\x00-\x1F\x7F]/g, '');
31
+ return chunk;
32
+ }
11
33
  /**
12
34
  * Show a text input prompt
13
35
  * @param config - Input configuration
14
36
  * @returns Promise resolving to entered text
15
37
  */
16
38
  async function showTextInput(config) {
17
- const { prompt, defaultValue = '', placeholder, maxLength, minLength = 0, allowEmpty = false, validate, errorMessage, onExit } = config;
39
+ const { lang: langInput, prompt, defaultValue = '', placeholder, maxLength, minLength = 0, allowEmpty = false, validate, errorMessage, onExit, preserveOnExit = false } = config;
40
+ const lang = langInput ?? (0, registry_js_1.getCurrentLanguage)();
18
41
  const state = (0, terminal_js_1.initTerminal)();
19
42
  let inputValue = '';
20
43
  let errorMsg = '';
@@ -24,17 +47,18 @@ async function showTextInput(config) {
24
47
  // Render prompt with default value hint
25
48
  let promptLine = ` ${prompt}`;
26
49
  if (defaultValue && !inputValue) {
27
- promptLine += ` ${colors_js_1.colors.dim}(默认: ${defaultValue})${colors_js_1.colors.reset}`;
50
+ const defaultLabel = lang === 'en' ? 'default' : (0, registry_js_1.t)('inputs.defaultValue');
51
+ promptLine += ` ${colors_js_1.uiColors.textSecondary}(${defaultLabel}: ${defaultValue})${colors_js_1.colors.reset}`;
28
52
  }
29
53
  if (placeholder && !inputValue) {
30
- promptLine += ` ${colors_js_1.colors.dim}${placeholder}${colors_js_1.colors.reset}`;
54
+ promptLine += ` ${colors_js_1.uiColors.textSecondary}${placeholder}${colors_js_1.colors.reset}`;
31
55
  }
32
- promptLine += `: ${colors_js_1.colors.cyan}${inputValue}_${colors_js_1.colors.reset}`;
56
+ promptLine += `: ${colors_js_1.uiColors.primary}${inputValue}_${colors_js_1.colors.reset}`;
33
57
  (0, terminal_js_1.writeLine)(promptLine);
34
58
  lineCount++;
35
59
  // Render error message if any
36
60
  if (errorMsg) {
37
- (0, terminal_js_1.writeLine)(` ${colors_js_1.colors.red}✗ ${errorMsg}${colors_js_1.colors.reset}`);
61
+ (0, terminal_js_1.writeLine)(` ${colors_js_1.uiColors.error}✗ ${errorMsg}${colors_js_1.colors.reset}`);
38
62
  lineCount++;
39
63
  }
40
64
  state.renderedLines = lineCount;
@@ -47,13 +71,15 @@ async function showTextInput(config) {
47
71
  // Handle Ctrl+C
48
72
  if ((0, keyboard_js_1.isCtrlC)(key)) {
49
73
  state.stdin.removeListener('data', onData);
50
- (0, terminal_js_1.clearMenu)(state);
74
+ if (!preserveOnExit) {
75
+ (0, terminal_js_1.clearMenu)(state);
76
+ }
51
77
  (0, terminal_js_1.restoreTerminal)(state);
52
78
  if (onExit) {
53
79
  onExit();
54
80
  }
55
81
  else {
56
- console.log('\n👋 再见!');
82
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
57
83
  }
58
84
  process.exit(0);
59
85
  }
@@ -62,13 +88,13 @@ async function showTextInput(config) {
62
88
  const finalValue = inputValue || defaultValue;
63
89
  // Check if empty
64
90
  if (!allowEmpty && !finalValue) {
65
- errorMsg = errorMessage || '输入不能为空';
91
+ errorMsg = errorMessage || (0, registry_js_1.t)('inputs.cannotBeEmpty');
66
92
  render();
67
93
  return;
68
94
  }
69
95
  // Check min length
70
96
  if (minLength && finalValue.length < minLength) {
71
- errorMsg = errorMessage || `输入长度不能少于 ${minLength} 个字符`;
97
+ errorMsg = errorMessage || `${(0, registry_js_1.t)('inputs.minLength')}: ${minLength}`;
72
98
  render();
73
99
  return;
74
100
  }
@@ -76,7 +102,7 @@ async function showTextInput(config) {
76
102
  if (validate) {
77
103
  const validationResult = validate(finalValue);
78
104
  if (validationResult !== true) {
79
- errorMsg = typeof validationResult === 'string' ? validationResult : (errorMessage || '输入无效');
105
+ errorMsg = typeof validationResult === 'string' ? validationResult : (errorMessage || (0, registry_js_1.t)('inputs.invalidInput'));
80
106
  render();
81
107
  return;
82
108
  }
@@ -97,13 +123,24 @@ async function showTextInput(config) {
97
123
  }
98
124
  return;
99
125
  }
100
- // Handle printable characters
101
- if ((0, keyboard_js_1.isPrintable)(key)) {
126
+ // Handle single-key typing and multi-char paste chunks.
127
+ const chunk = (0, keyboard_js_1.isPrintable)(key) ? key : normalizeTextChunk(key);
128
+ if (chunk.length > 0) {
102
129
  // Check max length
103
130
  if (maxLength && inputValue.length >= maxLength) {
104
131
  return;
105
132
  }
106
- inputValue += key;
133
+ let next = chunk;
134
+ if (maxLength) {
135
+ const remaining = maxLength - inputValue.length;
136
+ if (remaining <= 0) {
137
+ return;
138
+ }
139
+ if (next.length > remaining) {
140
+ next = next.slice(0, remaining);
141
+ }
142
+ }
143
+ inputValue += next;
107
144
  errorMsg = ''; // Clear error on edit
108
145
  render();
109
146
  return;
@@ -15,30 +15,36 @@ const registry_js_1 = require("../../i18n/registry.js");
15
15
  * @returns Promise resolving to boolean result
16
16
  */
17
17
  async function showBooleanMenu(config) {
18
- const { question, defaultValue = true, yesText = (0, registry_js_1.t)('menus.yes'), noText = (0, registry_js_1.t)('menus.no'), orientation = 'horizontal', onExit, preserveOnSelect = false } = config;
18
+ const { question, helperText, defaultValue = true, yesText = (0, registry_js_1.t)('menus.yes'), noText = (0, registry_js_1.t)('menus.no'), orientation = 'horizontal', onExit, preserveOnSelect = false } = config;
19
+ const preserveOnExit = config.preserveOnExit ?? preserveOnSelect;
19
20
  if (orientation === 'horizontal') {
20
- return showBooleanMenuHorizontal(question, defaultValue, yesText, noText, onExit, preserveOnSelect);
21
+ return showBooleanMenuHorizontal(question, helperText, defaultValue, yesText, noText, onExit, preserveOnSelect, preserveOnExit);
21
22
  }
22
23
  else {
23
- return showBooleanMenuVertical(question, defaultValue, yesText, noText, onExit, preserveOnSelect);
24
+ return showBooleanMenuVertical(question, helperText, defaultValue, yesText, noText, onExit, preserveOnSelect, preserveOnExit);
24
25
  }
25
26
  }
26
27
  /**
27
28
  * Show horizontal boolean menu (side by side)
28
29
  */
29
- async function showBooleanMenuHorizontal(question, defaultValue, yesText, noText, onExit, preserveOnSelect = false) {
30
+ async function showBooleanMenuHorizontal(question, helperText, defaultValue, yesText, noText, onExit, preserveOnSelect = false, preserveOnExit = false) {
30
31
  const state = (0, terminal_js_1.initTerminal)();
31
32
  let selected = defaultValue;
32
33
  const render = () => {
33
34
  (0, terminal_js_1.clearMenu)(state);
34
35
  // Render question with options on same line
35
36
  const yesOption = selected
36
- ? `${colors_js_1.colors.cyan}${yesText}${colors_js_1.colors.reset}`
37
- : `${colors_js_1.colors.dim}${yesText}${colors_js_1.colors.reset}`;
37
+ ? `${colors_js_1.uiColors.primary}${yesText}${colors_js_1.colors.reset}`
38
+ : `${colors_js_1.uiColors.textSecondary}${yesText}${colors_js_1.colors.reset}`;
38
39
  const noOption = !selected
39
- ? `${colors_js_1.colors.cyan}${noText}${colors_js_1.colors.reset}`
40
- : `${colors_js_1.colors.dim}${noText}${colors_js_1.colors.reset}`;
41
- (0, terminal_js_1.writeLine)(`${colors_js_1.colors.yellow}?${colors_js_1.colors.reset} ${question} ${yesOption} | ${noOption}`);
40
+ ? `${colors_js_1.uiColors.primary}${noText}${colors_js_1.colors.reset}`
41
+ : `${colors_js_1.uiColors.textSecondary}${noText}${colors_js_1.colors.reset}`;
42
+ (0, terminal_js_1.writeLine)(`${colors_js_1.uiColors.warning}?${colors_js_1.colors.reset} ${question} ${yesOption} | ${noOption}`);
43
+ if (helperText && helperText.trim().length > 0) {
44
+ (0, terminal_js_1.writeLine)(` ${colors_js_1.uiColors.textSecondary}${helperText}${colors_js_1.colors.reset}`);
45
+ state.renderedLines = 2;
46
+ return;
47
+ }
42
48
  state.renderedLines = 1;
43
49
  };
44
50
  // Initial render
@@ -49,13 +55,15 @@ async function showBooleanMenuHorizontal(question, defaultValue, yesText, noText
49
55
  // Handle Ctrl+C
50
56
  if ((0, keyboard_js_1.isCtrlC)(key)) {
51
57
  state.stdin.removeListener('data', onData);
52
- (0, terminal_js_1.clearMenu)(state);
58
+ if (!preserveOnExit) {
59
+ (0, terminal_js_1.clearMenu)(state);
60
+ }
53
61
  (0, terminal_js_1.restoreTerminal)(state);
54
62
  if (onExit) {
55
63
  onExit();
56
64
  }
57
65
  else {
58
- console.log('\n👋 再见!');
66
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
59
67
  }
60
68
  process.exit(0);
61
69
  }
@@ -99,23 +107,26 @@ async function showBooleanMenuHorizontal(question, defaultValue, yesText, noText
99
107
  /**
100
108
  * Show vertical boolean menu (stacked)
101
109
  */
102
- async function showBooleanMenuVertical(question, defaultValue, yesText, noText, onExit, preserveOnSelect = false) {
110
+ async function showBooleanMenuVertical(question, helperText, defaultValue, yesText, noText, onExit, preserveOnSelect = false, preserveOnExit = false) {
103
111
  const state = (0, terminal_js_1.initTerminal)();
104
112
  let selected = defaultValue;
105
113
  const render = () => {
106
114
  (0, terminal_js_1.clearMenu)(state);
107
115
  // Render question
108
- (0, terminal_js_1.writeLine)(`${colors_js_1.colors.yellow}?${colors_js_1.colors.reset} ${question}`);
116
+ (0, terminal_js_1.writeLine)(`${colors_js_1.uiColors.warning}?${colors_js_1.colors.reset} ${question}`);
117
+ if (helperText && helperText.trim().length > 0) {
118
+ (0, terminal_js_1.writeLine)(` ${colors_js_1.uiColors.textSecondary}${helperText}${colors_js_1.colors.reset}`);
119
+ }
109
120
  (0, terminal_js_1.writeLine)('');
110
121
  // Render yes option
111
- const yesCursor = selected ? `${colors_js_1.colors.cyan}❯ ${colors_js_1.colors.reset}` : ' ';
112
- const yesColor = selected ? colors_js_1.colors.cyan : colors_js_1.colors.reset;
122
+ const yesCursor = selected ? `${colors_js_1.uiColors.cursor}❯ ${colors_js_1.colors.reset}` : ' ';
123
+ const yesColor = selected ? colors_js_1.uiColors.primary : colors_js_1.uiColors.textPrimary;
113
124
  (0, terminal_js_1.writeLine)(`${yesCursor}${yesColor}${yesText}${colors_js_1.colors.reset}`);
114
125
  // Render no option
115
- const noCursor = !selected ? `${colors_js_1.colors.cyan}❯ ${colors_js_1.colors.reset}` : ' ';
116
- const noColor = !selected ? colors_js_1.colors.cyan : colors_js_1.colors.reset;
126
+ const noCursor = !selected ? `${colors_js_1.uiColors.cursor}❯ ${colors_js_1.colors.reset}` : ' ';
127
+ const noColor = !selected ? colors_js_1.uiColors.primary : colors_js_1.uiColors.textPrimary;
117
128
  (0, terminal_js_1.writeLine)(`${noCursor}${noColor}${noText}${colors_js_1.colors.reset}`);
118
- state.renderedLines = 4;
129
+ state.renderedLines = helperText && helperText.trim().length > 0 ? 5 : 4;
119
130
  };
120
131
  // Initial render
121
132
  render();
@@ -125,13 +136,15 @@ async function showBooleanMenuVertical(question, defaultValue, yesText, noText,
125
136
  // Handle Ctrl+C
126
137
  if ((0, keyboard_js_1.isCtrlC)(key)) {
127
138
  state.stdin.removeListener('data', onData);
128
- (0, terminal_js_1.clearMenu)(state);
139
+ if (!preserveOnExit) {
140
+ (0, terminal_js_1.clearMenu)(state);
141
+ }
129
142
  (0, terminal_js_1.restoreTerminal)(state);
130
143
  if (onExit) {
131
144
  onExit();
132
145
  }
133
146
  else {
134
- console.log('\n👋 再见!');
147
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
135
148
  }
136
149
  process.exit(0);
137
150
  }
@@ -17,6 +17,7 @@ const registry_js_1 = require("../../i18n/registry.js");
17
17
  */
18
18
  async function showCheckboxMenu(config, hints) {
19
19
  const { options, title, prompt, defaultSelected = [], minSelections = 0, maxSelections, allowSelectAll = true, allowInvert = true, separatorWidth = 30, onExit, preserveOnSelect = false } = config;
20
+ const preserveOnExit = config.preserveOnExit ?? preserveOnSelect;
20
21
  // Use i18n for default prompt if not provided
21
22
  const displayPrompt = prompt || (0, registry_js_1.t)('menus.multiSelectPrompt');
22
23
  // Validate options
@@ -113,13 +114,15 @@ async function showCheckboxMenu(config, hints) {
113
114
  // Handle Ctrl+C
114
115
  if ((0, keyboard_js_1.isCtrlC)(key)) {
115
116
  state.stdin.removeListener('data', onData);
116
- (0, terminal_js_1.clearMenu)(state);
117
+ if (!preserveOnExit) {
118
+ (0, terminal_js_1.clearMenu)(state);
119
+ }
117
120
  (0, terminal_js_1.restoreTerminal)(state);
118
121
  if (onExit) {
119
122
  onExit();
120
123
  }
121
124
  else {
122
- console.log('\n👋 再见!');
125
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
123
126
  }
124
127
  process.exit(0);
125
128
  }