mepcli 0.2.7 → 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 +5 -0
- package/dist/base.js +20 -157
- package/dist/core.js +4 -3
- package/dist/prompts/checkbox.js +5 -4
- package/dist/prompts/date.js +2 -1
- package/dist/prompts/file.js +3 -2
- package/dist/prompts/list.js +2 -1
- package/dist/prompts/multi-select.js +5 -4
- package/dist/prompts/number.js +2 -1
- package/dist/prompts/select.js +3 -2
- package/dist/prompts/slider.js +2 -1
- package/dist/prompts/text.js +2 -1
- package/dist/symbols.d.ts +17 -0
- package/dist/symbols.js +25 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +35 -7
- package/package.json +4 -4
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
|
|
104
|
+
// Truncate each line to fit terminal width to avoid wrapping issues
|
|
101
105
|
const newLines = rawLines.map(line => this.truncate(line, width));
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
//
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
//
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
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.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}
|
|
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}
|
|
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
|
}
|
package/dist/prompts/checkbox.js
CHANGED
|
@@ -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}
|
|
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}
|
|
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}
|
|
44
|
-
: `${theme_1.theme.muted}
|
|
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;
|
package/dist/prompts/date.js
CHANGED
|
@@ -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}
|
|
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) {
|
package/dist/prompts/file.js
CHANGED
|
@@ -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}
|
|
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}
|
|
111
|
+
output += `${theme_1.theme.main}${symbols_1.symbols.pointer} ${s}${ansi_1.ANSI.RESET}`;
|
|
111
112
|
}
|
|
112
113
|
else {
|
|
113
114
|
output += ` ${s}`;
|
package/dist/prompts/list.js
CHANGED
|
@@ -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}
|
|
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) {
|
|
@@ -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}
|
|
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}
|
|
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}
|
|
55
|
-
: `${theme_1.theme.muted}
|
|
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
|
}
|
package/dist/prompts/number.js
CHANGED
|
@@ -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}
|
|
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
|
package/dist/prompts/select.js
CHANGED
|
@@ -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 ||
|
|
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}
|
|
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}`;
|
package/dist/prompts/slider.js
CHANGED
|
@@ -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}`;
|
package/dist/prompts/text.js
CHANGED
|
@@ -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}
|
|
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;
|
package/dist/symbols.js
ADDED
|
@@ -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/utils.d.ts
CHANGED
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
|
|
18
|
-
|
|
17
|
+
// Check if it is a TTY
|
|
18
|
+
const isTTY = process.stdout.isTTY;
|
|
19
19
|
const isWindows = process.platform === 'win32';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
34
|
-
"ts-node": "^10
|
|
35
|
-
"typescript": "^5
|
|
33
|
+
"@types/node": "^22",
|
|
34
|
+
"ts-node": "^10",
|
|
35
|
+
"typescript": "^5"
|
|
36
36
|
}
|
|
37
37
|
}
|