mepcli 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/base.d.ts CHANGED
@@ -47,6 +47,11 @@ export declare abstract class Prompt<T, O> {
47
47
  * Render Method with Diffing (Virtual DOM for CLI).
48
48
  * Calculates new lines, compares with old lines, and updates only changed parts.
49
49
  */
50
+ /**
51
+ * Renders the current frame by clearing the previous output and writing the new content.
52
+ * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
53
+ * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
54
+ */
50
55
  protected renderFrame(content: string): void;
51
56
  protected stripAnsi(str: string): string;
52
57
  protected truncate(str: string, width: number): string;
package/dist/base.js CHANGED
@@ -93,170 +93,37 @@ class Prompt {
93
93
  * Render Method with Diffing (Virtual DOM for CLI).
94
94
  * Calculates new lines, compares with old lines, and updates only changed parts.
95
95
  */
96
+ /**
97
+ * Renders the current frame by clearing the previous output and writing the new content.
98
+ * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
99
+ * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
100
+ */
96
101
  renderFrame(content) {
97
- // Ensure lines are truncated to terminal width
98
102
  const width = this.stdout.columns || 80;
99
103
  const rawLines = content.split('\n');
100
- // Truncate each line and prepare the new buffer
104
+ // Truncate each line to fit terminal width to avoid wrapping issues
101
105
  const newLines = rawLines.map(line => this.truncate(line, width));
102
- // Cursor logic:
103
- // We assume the cursor is currently at the END of the last rendered frame.
104
- // But to diff, it's easier to always reset to the top of the frame first.
105
- // 1. Move Cursor to Top of the Frame
106
+ // 1. Move cursor to the start of the current line
107
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
108
+ // 2. Move cursor up to the top of the previously rendered frame
106
109
  if (this.lastRenderHeight > 0) {
107
- this.print(`\x1b[${this.lastRenderHeight}A`); // Move up N lines
108
- // Actually, if last height was 1 (just one line), we move up 1 line?
109
- // "A\n" -> 2 lines. Cursor at bottom.
110
- // If we move up, we are at top.
111
- // Wait, if lastRenderHeight includes the "current line" which is usually empty if we printed with newlines?
112
- // Let's stick to: we printed N lines. Cursor is at line N+1 (start).
113
- // To go to line 1, we move up N lines.
114
- this.print(`\x1b[${this.lastRenderHeight}A`);
110
+ // If the previous render had multiple lines, move up to the first line
111
+ if (this.lastRenderHeight > 1) {
112
+ this.print(`\x1b[${this.lastRenderHeight - 1}A`);
113
+ }
115
114
  }
116
- this.print(ansi_1.ANSI.CURSOR_LEFT);
117
- // 2. Diff and Render
118
- // We iterate through newLines.
119
- // For each line, check if it matches lastRenderLines[i].
115
+ // 3. Clear everything from the cursor down
116
+ // This ensures all previous content (including "ghost" lines) is removed
117
+ this.print(ansi_1.ANSI.ERASE_DOWN);
118
+ // 4. Print the new frame content
120
119
  for (let i = 0; i < newLines.length; i++) {
121
- const newLine = newLines[i];
122
- const oldLine = this.lastRenderLines[i];
123
- if (newLine !== oldLine) {
124
- // Move to this line if not already there?
125
- // We are writing sequentially, so after writing line i-1 (or skipping it),
126
- // the cursor might not be at the start of line i if we skipped.
127
- // Strategy:
128
- // If we skipped lines, we need to jump down.
129
- // But simpler: just move cursor to line i relative to top.
130
- // \x1b[<N>B moves down N lines.
131
- // But we are processing sequentially.
132
- // If we are at line 0 (Top).
133
- // Process line 0.
134
- // If changed, write it + \n (or clear line + write).
135
- // If unchanged, move cursor down 1 line.
136
- // Wait, if we use \n at end of write, cursor moves down.
137
- // If we skip writing, we must manually move down.
138
- this.print(ansi_1.ANSI.ERASE_LINE); // Clear current line
139
- this.print(newLine);
140
- }
141
- // Prepare for next line
120
+ this.print(newLines[i]);
121
+ // Add newline character between lines, but not after the last line
142
122
  if (i < newLines.length - 1) {
143
- // If we wrote something, we are at end of line (maybe wrapped?).
144
- // Since we truncate, we are not wrapped.
145
- // But we didn't print \n yet if we just printed newLine.
146
- // To move to next line start:
147
123
  this.print('\n');
148
124
  }
149
125
  }
150
- // 3. Clear remaining lines if new output is shorter
151
- if (newLines.length < this.lastRenderLines.length) {
152
- // We are at the last line of new output.
153
- // BUG FIX: If the last line was unchanged, we skipped printing.
154
- // The cursor is currently at the START of that line (or end of previous).
155
- // We need to ensure we move to the NEXT line (or end of current) before clearing down.
156
- // If we just finished loop `i = newLines.length - 1`, we are theoretically at the end of the content.
157
- // However, since we might have skipped the last line, we need to be careful.
158
- // Let's force move to the end of the visual content we just defined.
159
- // Actually, simplest way: Just move cursor to start of line N (where N = newLines.length).
160
- // Currently we are at line newLines.length - 1.
161
- // We need to move down 1 line?
162
- // If newLines has 1 line. Loop runs 0.
163
- // If skipped, we are at start of line 0.
164
- // We need to be at line 1 to clear from there down.
165
- // But we didn't print \n.
166
- // So: move cursor to (newLines.length) relative to top.
167
- // We started at Top.
168
- // We iterated newLines.
169
- // We injected \n between lines.
170
- // The cursor is implicitly tracking where we are.
171
- // IF we skipped, we are physically at start of line `i`.
172
- // We need to move over it.
173
- // Fix: After the loop, explicitly move to the line AFTER the last line.
174
- // Since we know where we started (Top), we can just jump to line `newLines.length`.
175
- // But we are in relative movement land.
176
- // Let's calculate where we *should* be: End of content.
177
- // If we just rendered N lines, we want to be at line N+1 (conceptually) to clear below?
178
- // Or just at the start of line N+1?
179
- // If we have 2 lines.
180
- // Line 0. \n. Line 1.
181
- // Cursor is at end of Line 1.
182
- // If we skipped Line 1, cursor is at start of Line 1.
183
- // We want to clear everything BELOW Line 1.
184
- // So we should be at start of Line 2.
185
- // Logic:
186
- // 1. We are currently at the cursor position after processing `newLines`.
187
- // If last line was skipped, we are at start of last line.
188
- // If last line was written, we are at end of last line.
189
- // 2. We want to erase from the line *following* the last valid line.
190
- // We can just calculate the difference and move down if needed.
191
- // But simpler: Move cursor to the conceptual "end" of the new frame.
192
- // If we processed `newLines.length` lines.
193
- // We want to be at row `newLines.length` (0-indexed) to start clearing?
194
- // No, rows are 0 to N-1.
195
- // We want to clear starting from row N.
196
- // Since we can't easily query cursor pos, let's use the fact we reset to Top.
197
- // We can move to row N relative to current?
198
- // Wait, `ERASE_DOWN` clears from cursor to end of screen.
199
- // If we are at start of Line 1 (and it's valid), `ERASE_DOWN` deletes Line 1!
200
- // So we MUST be past Line 1.
201
- // If we skipped the last line, we must strictly move past it.
202
- // How? `\x1b[1B` (Down).
203
- // But we don't track if we skipped the last line explicitly outside the loop.
204
- // Let's just track `currentLineIndex`.
205
- // Alternate robust approach:
206
- // After loop, we forcefully move cursor to `newLines.length` lines down from Top.
207
- // We are currently at some unknown state (Start or End of last line).
208
- // BUT we can just move UP to Top again and then move DOWN N lines.
209
- // That feels safe.
210
- // Reset to top of frame (which we are already inside/near).
211
- // But we don't know exactly where we are relative to top anymore.
212
- // Let's rely on the loop index.
213
- // If loop finished, `i` was `newLines.length`.
214
- // If `newLines.length > 0`.
215
- // If we skipped the last line (index `len-1`), we are at start of it.
216
- // If we wrote it, we are at end of it.
217
- // If we skipped, we need `\x1b[1B`.
218
- // If we wrote, we are at end. `ERASE_DOWN` from end of line clears rest of line + below.
219
- // BUT we want to clear BELOW.
220
- // `ERASE_DOWN` (J=0) clears from cursor to end of screen.
221
- // If at end of line, it clears rest of that line (nothing) and lines below. Correct.
222
- // So the issue is ONLY when we skipped the last line.
223
- const lastLineIdx = newLines.length - 1;
224
- if (lastLineIdx >= 0 && newLines[lastLineIdx] === this.lastRenderLines[lastLineIdx]) {
225
- // We skipped the last line. Move down 1 line to ensure we don't delete it.
226
- // Also move to start (CR) to be safe?
227
- this.print('\n');
228
- // Wait, \n moves down AND to start usually.
229
- // But strictly \n is Line Feed (Down). \r is Carriage Return (Left).
230
- // Console usually treats \n as \r\n in cooked mode, but in raw mode?
231
- // We are in raw mode.
232
- // We likely need \r\n or explicit movement.
233
- // Let's just use \x1b[1B (Down) and \r (Left).
234
- // Actually, if we use `\n` in loop, we rely on it working.
235
- // Let's assume `\x1b[1B` is safer for "just move down".
236
- // But wait, if we are at start of line, `1B` puts us at start of next line.
237
- // `ERASE_DOWN` there is perfect.
238
- this.print('\x1b[1B');
239
- this.print('\r'); // Move to start
240
- }
241
- else {
242
- // We wrote the last line. We are at the end of it.
243
- // `ERASE_DOWN` will clear lines below.
244
- // BUT if we want to clear the REST of the screen cleanly starting from next line...
245
- // It's mostly fine.
246
- // However, there is a subtle case:
247
- // If we wrote the line, we are at the end of it.
248
- // If we call ERASE_DOWN, it keeps current line intact (from cursor onwards, which is empty).
249
- // And clears below.
250
- // This is correct.
251
- // EXCEPT: If the old screen had MORE lines, we want to clear them.
252
- // If we are at end of Line N-1.
253
- // Line N exists in old screen.
254
- // ERASE_DOWN clears Line N etc.
255
- // Correct.
256
- }
257
- this.print(ansi_1.ANSI.ERASE_DOWN);
258
- }
259
- // Update state
126
+ // 5. Update state for the next render cycle
260
127
  this.lastRenderLines = newLines;
261
128
  this.lastRenderHeight = newLines.length;
262
129
  }
@@ -272,10 +139,6 @@ class Prompt {
272
139
  // We iterate and sum width until we hit limit - 3
273
140
  let currentWidth = 0;
274
141
  let cutIndex = 0;
275
- // We need to iterate by Code Point or Grapheme to be safe?
276
- // Let's use simple char iteration for speed, but respect ANSI.
277
- // Actually, reusing the logic from stringWidth might be best but
278
- // we need the index.
279
142
  let inAnsi = false;
280
143
  for (let i = 0; i < str.length; i++) {
281
144
  const code = str.charCodeAt(i);
package/dist/core.d.ts CHANGED
@@ -9,8 +9,8 @@ export declare class MepCLI {
9
9
  */
10
10
  static spin<T>(message: string, taskPromise: Promise<T>): Promise<T>;
11
11
  static text(options: TextOptions): Promise<string>;
12
- static select(options: SelectOptions): Promise<any>;
13
- static checkbox(options: CheckboxOptions): Promise<any[]>;
12
+ static select<const V>(options: SelectOptions<V>): Promise<V>;
13
+ static checkbox<const V>(options: CheckboxOptions<V>): Promise<V[]>;
14
14
  static confirm(options: ConfirmOptions): Promise<boolean>;
15
15
  static password(options: TextOptions): Promise<string>;
16
16
  static number(options: NumberOptions): Promise<number>;
@@ -19,5 +19,5 @@ export declare class MepCLI {
19
19
  static slider(options: SliderOptions): Promise<number>;
20
20
  static date(options: DateOptions): Promise<Date>;
21
21
  static file(options: FileOptions): Promise<string>;
22
- static multiSelect(options: MultiSelectOptions): Promise<any[]>;
22
+ static multiSelect<const V>(options: MultiSelectOptions<V>): Promise<V[]>;
23
23
  }
package/dist/core.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MepCLI = void 0;
4
4
  const ansi_1 = require("./ansi");
5
5
  const theme_1 = require("./theme");
6
+ const symbols_1 = require("./symbols");
6
7
  const text_1 = require("./prompts/text");
7
8
  const select_1 = require("./prompts/select");
8
9
  const checkbox_1 = require("./prompts/checkbox");
@@ -22,7 +23,7 @@ class MepCLI {
22
23
  * Shows a spinner while a promise is pending.
23
24
  */
24
25
  static async spin(message, taskPromise) {
25
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
26
+ const frames = symbols_1.symbols.spinner;
26
27
  let i = 0;
27
28
  process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
28
29
  const interval = setInterval(() => {
@@ -32,13 +33,13 @@ class MepCLI {
32
33
  try {
33
34
  const result = await taskPromise;
34
35
  clearInterval(interval);
35
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}✔${ansi_1.ANSI.RESET} ${message}\n`);
36
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}${symbols_1.symbols.tick}${ansi_1.ANSI.RESET} ${message}\n`);
36
37
  process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
37
38
  return result;
38
39
  }
39
40
  catch (error) {
40
41
  clearInterval(interval);
41
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}✖${ansi_1.ANSI.RESET} ${message}\n`);
42
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}${symbols_1.symbols.cross}${ansi_1.ANSI.RESET} ${message}\n`);
42
43
  process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
43
44
  throw error;
44
45
  }
@@ -1,12 +1,12 @@
1
1
  import { Prompt } from '../base';
2
2
  import { CheckboxOptions } from '../types';
3
- export declare class CheckboxPrompt extends Prompt<any[], CheckboxOptions> {
3
+ export declare class CheckboxPrompt<V> extends Prompt<any[], CheckboxOptions<V>> {
4
4
  private selectedIndex;
5
5
  private checkedState;
6
6
  private errorMsg;
7
7
  private scrollTop;
8
8
  private readonly pageSize;
9
- constructor(options: CheckboxOptions);
9
+ constructor(options: CheckboxOptions<V>);
10
10
  protected render(firstRender: boolean): void;
11
11
  protected handleInput(char: string): void;
12
12
  }
@@ -4,6 +4,7 @@ exports.CheckboxPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Checkbox Prompt ---
8
9
  class CheckboxPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -29,7 +30,7 @@ class CheckboxPrompt extends base_1.Prompt {
29
30
  }
30
31
  let output = '';
31
32
  // Header
32
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
33
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
33
34
  output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${theme_1.theme.muted}(Press <space> to select, <enter> to confirm)${ansi_1.ANSI.RESET}`;
34
35
  // List
35
36
  const choices = this.options.choices;
@@ -37,11 +38,11 @@ class CheckboxPrompt extends base_1.Prompt {
37
38
  visibleChoices.forEach((choice, index) => {
38
39
  const actualIndex = this.scrollTop + index;
39
40
  output += '\n'; // New line for each item
40
- const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
41
+ const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}${symbols_1.symbols.pointer}${ansi_1.ANSI.RESET}` : ' ';
41
42
  const isChecked = this.checkedState[actualIndex];
42
43
  const checkbox = isChecked
43
- ? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
44
- : `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
44
+ ? `${theme_1.theme.success}${symbols_1.symbols.checked}${ansi_1.ANSI.RESET}`
45
+ : `${theme_1.theme.muted}${symbols_1.symbols.unchecked}${ansi_1.ANSI.RESET}`;
45
46
  const title = actualIndex === this.selectedIndex
46
47
  ? `${theme_1.theme.main}${choice.title}${ansi_1.ANSI.RESET}`
47
48
  : choice.title;
@@ -4,6 +4,7 @@ exports.DatePrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Date Prompt ---
8
9
  class DatePrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -26,7 +27,7 @@ class DatePrompt extends base_1.Prompt {
26
27
  return `${theme_1.theme.main}${ansi_1.ANSI.UNDERLINE}${val}${ansi_1.ANSI.RESET}`;
27
28
  return val;
28
29
  });
29
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
30
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
30
31
  const dateStr = `${display[0]}-${display[1]}-${display[2]} ${display[3]}:${display[4]}`;
31
32
  let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${dateStr} ${theme_1.theme.muted}(Use arrows or type)${ansi_1.ANSI.RESET}`;
32
33
  if (this.errorMsg) {
@@ -37,6 +37,7 @@ exports.FilePrompt = void 0;
37
37
  const ansi_1 = require("../ansi");
38
38
  const base_1 = require("../base");
39
39
  const theme_1 = require("../theme");
40
+ const symbols_1 = require("../symbols");
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  // --- Implementation: File Prompt ---
@@ -96,7 +97,7 @@ class FilePrompt extends base_1.Prompt {
96
97
  }
97
98
  render(firstRender) {
98
99
  // Construct string
99
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
100
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
100
101
  let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`;
101
102
  // Suggestions
102
103
  if (this.suggestions.length > 0) {
@@ -107,7 +108,7 @@ class FilePrompt extends base_1.Prompt {
107
108
  if (i > 0)
108
109
  output += '\n';
109
110
  if (i === this.selectedSuggestion) {
110
- output += `${theme_1.theme.main} ${s}${ansi_1.ANSI.RESET}`;
111
+ output += `${theme_1.theme.main}${symbols_1.symbols.pointer} ${s}${ansi_1.ANSI.RESET}`;
111
112
  }
112
113
  else {
113
114
  output += ` ${s}`;
@@ -4,6 +4,7 @@ exports.ListPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: List Prompt ---
8
9
  class ListPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -14,7 +15,7 @@ class ListPrompt extends base_1.Prompt {
14
15
  }
15
16
  render(firstRender) {
16
17
  // Prepare content
17
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
18
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
18
19
  let mainLine = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
19
20
  // Render Tags
20
21
  if (this.value.length > 0) {
@@ -1,13 +1,13 @@
1
1
  import { Prompt } from '../base';
2
2
  import { MultiSelectOptions } from '../types';
3
- export declare class MultiSelectPrompt extends Prompt<any[], MultiSelectOptions> {
3
+ export declare class MultiSelectPrompt<V> extends Prompt<any[], MultiSelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private checkedState;
6
6
  private searchBuffer;
7
7
  private scrollTop;
8
8
  private readonly pageSize;
9
9
  private errorMsg;
10
- constructor(options: MultiSelectOptions);
10
+ constructor(options: MultiSelectOptions<V>);
11
11
  private getFilteredChoices;
12
12
  protected render(firstRender: boolean): void;
13
13
  protected handleInput(char: string): void;
@@ -4,6 +4,7 @@ exports.MultiSelectPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: MultiSelect Prompt ---
8
9
  class MultiSelectPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -36,7 +37,7 @@ class MultiSelectPrompt extends base_1.Prompt {
36
37
  if (this.scrollTop > choices.length - 1) {
37
38
  this.scrollTop = Math.max(0, choices.length - this.pageSize);
38
39
  }
39
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
40
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
40
41
  const searchStr = this.searchBuffer ? ` ${theme_1.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
41
42
  output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
42
43
  if (choices.length === 0) {
@@ -48,11 +49,11 @@ class MultiSelectPrompt extends base_1.Prompt {
48
49
  if (index > 0)
49
50
  output += '\n';
50
51
  const actualIndex = this.scrollTop + index;
51
- const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
52
+ const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}${symbols_1.symbols.pointer}${ansi_1.ANSI.RESET}` : ' ';
52
53
  const isChecked = this.checkedState[choice.originalIndex];
53
54
  const checkbox = isChecked
54
- ? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
55
- : `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
55
+ ? `${theme_1.theme.success}${symbols_1.symbols.checked}${ansi_1.ANSI.RESET}`
56
+ : `${theme_1.theme.muted}${symbols_1.symbols.unchecked}${ansi_1.ANSI.RESET}`;
56
57
  output += `${cursor} ${checkbox} ${choice.title}`;
57
58
  });
58
59
  }
@@ -4,6 +4,7 @@ exports.NumberPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Number Prompt ---
8
9
  class NumberPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -18,7 +19,7 @@ class NumberPrompt extends base_1.Prompt {
18
19
  }
19
20
  render(firstRender) {
20
21
  // Prepare content
21
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
22
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
22
23
  // Prefix
23
24
  let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
24
25
  // Value
@@ -1,11 +1,11 @@
1
1
  import { Prompt } from '../base';
2
2
  import { SelectOptions } from '../types';
3
- export declare class SelectPrompt extends Prompt<any, SelectOptions> {
3
+ export declare class SelectPrompt<V> extends Prompt<any, SelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private searchBuffer;
6
6
  private scrollTop;
7
7
  private readonly pageSize;
8
- constructor(options: SelectOptions);
8
+ constructor(options: SelectOptions<V>);
9
9
  private isSeparator;
10
10
  private findNextSelectableIndex;
11
11
  private getFilteredChoices;
@@ -4,6 +4,7 @@ exports.SelectPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Select Prompt ---
8
9
  class SelectPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -80,11 +81,11 @@ class SelectPrompt extends base_1.Prompt {
80
81
  if (index > 0)
81
82
  output += '\n'; // Separator between items
82
83
  if (this.isSeparator(choice)) {
83
- output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}`;
84
+ output += ` ${ansi_1.ANSI.DIM}${choice.text || symbols_1.symbols.line.repeat(8)}${ansi_1.ANSI.RESET}`;
84
85
  }
85
86
  else {
86
87
  if (actualIndex === this.selectedIndex) {
87
- output += `${theme_1.theme.main} ${choice.title}${ansi_1.ANSI.RESET}`;
88
+ output += `${theme_1.theme.main}${symbols_1.symbols.pointer} ${choice.title}${ansi_1.ANSI.RESET}`;
88
89
  }
89
90
  else {
90
91
  output += ` ${choice.title}`;
@@ -4,6 +4,7 @@ exports.SliderPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Slider Prompt ---
8
9
  class SliderPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -20,7 +21,7 @@ class SliderPrompt extends base_1.Prompt {
20
21
  if (i === pos)
21
22
  bar += `${theme_1.theme.main}O${ansi_1.ANSI.RESET}`;
22
23
  else
23
- bar += '─';
24
+ bar += symbols_1.symbols.line;
24
25
  }
25
26
  const unit = this.options.unit || '';
26
27
  const output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} [${bar}] ${this.value}${unit}`;
@@ -4,6 +4,7 @@ exports.TextPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  const utils_1 = require("../utils");
8
9
  // --- Implementation: Text Prompt ---
9
10
  class TextPrompt extends base_1.Prompt {
@@ -23,7 +24,7 @@ class TextPrompt extends base_1.Prompt {
23
24
  // Calculate available width
24
25
  const cols = process.stdout.columns || 80;
25
26
  // 1. Prepare Prompt Label
26
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
27
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
27
28
  const hint = this.options.multiline ? ` ${theme_1.theme.muted}(Press Ctrl+D to submit)${ansi_1.ANSI.RESET}` : '';
28
29
  const prefix = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${hint} `;
29
30
  // We need visual length of prefix to calculate available space for input on the first line
@@ -0,0 +1,17 @@
1
+ export interface SymbolDefinition {
2
+ /** Used for success messages or valid states */
3
+ tick: string;
4
+ /** Used for error messages or invalid states */
5
+ cross: string;
6
+ /** Used to point to the current selection */
7
+ pointer: string;
8
+ /** Used for separators or sliders */
9
+ line: string;
10
+ /** Used for checked state in checkboxes/radio */
11
+ checked: string;
12
+ /** Used for unchecked state in checkboxes/radio */
13
+ unchecked: string;
14
+ /** Animation frames for the spinner */
15
+ spinner: string[];
16
+ }
17
+ export declare const symbols: SymbolDefinition;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.symbols = void 0;
4
+ const utils_1 = require("./utils");
5
+ const UnicodeSymbols = {
6
+ tick: '✔',
7
+ cross: '✖',
8
+ pointer: '❯',
9
+ line: '─',
10
+ checked: '◉',
11
+ unchecked: '◯',
12
+ spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
13
+ };
14
+ const AsciiSymbols = {
15
+ tick: '+',
16
+ cross: 'x',
17
+ pointer: '>',
18
+ line: '-',
19
+ checked: '[x]',
20
+ unchecked: '[ ]',
21
+ spinner: ['|', '/', '-', '\\']
22
+ };
23
+ const capabilities = (0, utils_1.detectCapabilities)();
24
+ const useUnicode = capabilities.hasUnicode && process.env.MEP_NO_UNICODE !== '1';
25
+ exports.symbols = useUnicode ? UnicodeSymbols : AsciiSymbols;
package/dist/types.d.ts CHANGED
@@ -22,19 +22,19 @@ export interface Separator {
22
22
  separator: true;
23
23
  text?: string;
24
24
  }
25
- export interface SelectChoice {
25
+ export interface SelectChoice<V> {
26
26
  title: string;
27
- value: any;
27
+ value: V;
28
28
  description?: string;
29
29
  }
30
- export interface SelectOptions extends BaseOptions {
31
- choices: (SelectChoice | Separator)[];
30
+ export interface SelectOptions<V> extends BaseOptions {
31
+ choices: (SelectChoice<V> | Separator)[];
32
32
  }
33
- export interface CheckboxChoice extends SelectChoice {
33
+ export interface CheckboxChoice<V> extends SelectChoice<V> {
34
34
  selected?: boolean;
35
35
  }
36
- export interface CheckboxOptions extends BaseOptions {
37
- choices: CheckboxChoice[];
36
+ export interface CheckboxOptions<V> extends BaseOptions {
37
+ choices: CheckboxChoice<V>[];
38
38
  min?: number;
39
39
  max?: number;
40
40
  }
@@ -75,5 +75,5 @@ export interface FileOptions extends BaseOptions {
75
75
  extensions?: string[];
76
76
  onlyDirectories?: boolean;
77
77
  }
78
- export interface MultiSelectOptions extends CheckboxOptions {
78
+ export interface MultiSelectOptions<V> extends CheckboxOptions<V> {
79
79
  }
package/dist/utils.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  export declare function detectCapabilities(): {
5
5
  isCI: boolean;
6
6
  hasTrueColor: boolean;
7
- hasUnicode: boolean;
7
+ hasUnicode: boolean | "";
8
8
  };
9
9
  /**
10
10
  * Strips ANSI escape codes from a string.
package/dist/utils.js CHANGED
@@ -14,17 +14,45 @@ function detectCapabilities() {
14
14
  const isCI = !!env.CI;
15
15
  // Check for True Color support
16
16
  const hasTrueColor = env.COLORTERM === 'truecolor';
17
- // Check for Unicode support
18
- // Windows Terminal (WT_SESSION), VS Code (TERM_PROGRAM=vscode), or modern Linux terminals usually support it.
17
+ // Check if it is a TTY
18
+ const isTTY = process.stdout.isTTY;
19
19
  const isWindows = process.platform === 'win32';
20
- const hasUnicode = !!env.WT_SESSION ||
21
- env.TERM_PROGRAM === 'vscode' ||
22
- env.TERM_PROGRAM === 'Apple_Terminal' ||
23
- (!isWindows && env.LANG && env.LANG.toUpperCase().endsWith('UTF-8'));
20
+ // Logic detect Unicode xịn hơn
21
+ const isUnicodeSupported = () => {
22
+ // 1. Windows: Check specific environmental variables
23
+ if (isWindows) {
24
+ // Windows Terminal
25
+ if (env.WT_SESSION)
26
+ return true;
27
+ // VSCode terminal
28
+ if (env.TERM_PROGRAM === 'vscode')
29
+ return true;
30
+ // Modern terminals setting TERM (e.g. Alacritty, Git Bash, Cygwin)
31
+ if (env.TERM === 'xterm-256color' || env.TERM === 'alacritty')
32
+ return true;
33
+ // ConEmu / Cmder
34
+ if (env.ConEmuTask)
35
+ return true;
36
+ // CI on Windows typically supports Unicode.
37
+ if (isCI)
38
+ return true;
39
+ // Default cmd.exe / old powershell => False (ASCII)
40
+ return false;
41
+ }
42
+ // 2. Non-Windows (Linux/macOS)
43
+ if (env.TERM_PROGRAM === 'Apple_Terminal')
44
+ return true;
45
+ // Check if the LANG or LC_ALL variable contains UTF-8.
46
+ const lang = env.LANG || '';
47
+ const lcAll = env.LC_ALL || '';
48
+ return (lang && lang.toUpperCase().endsWith('UTF-8')) ||
49
+ (lcAll && lcAll.toUpperCase().endsWith('UTF-8'));
50
+ };
24
51
  return {
25
52
  isCI,
26
53
  hasTrueColor,
27
- hasUnicode: hasUnicode || !isWindows // Assume non-windows has unicode by default if not strictly detected?
54
+ // Enable Unicode only if it's TTY and environment supports it.
55
+ hasUnicode: isTTY && isUnicodeSupported()
28
56
  };
29
57
  }
30
58
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,8 +30,8 @@
30
30
  "author": "CodeTease",
31
31
  "license": "MIT",
32
32
  "devDependencies": {
33
- "@types/node": "^20.19.27",
34
- "ts-node": "^10.9.2",
35
- "typescript": "^5.9.3"
33
+ "@types/node": "^22",
34
+ "ts-node": "^10",
35
+ "typescript": "^5"
36
36
  }
37
37
  }