mepcli 0.2.5 → 0.2.6
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 +13 -0
- package/dist/base.js +256 -2
- package/dist/input.d.ts +13 -0
- package/dist/input.js +89 -0
- package/dist/prompts/checkbox.d.ts +2 -0
- package/dist/prompts/checkbox.js +40 -18
- package/dist/prompts/confirm.js +9 -7
- package/dist/prompts/date.js +4 -25
- package/dist/prompts/file.js +50 -27
- package/dist/prompts/list.js +22 -17
- package/dist/prompts/multi-select.js +6 -32
- package/dist/prompts/number.d.ts +1 -1
- package/dist/prompts/number.js +30 -34
- package/dist/prompts/select.d.ts +0 -2
- package/dist/prompts/select.js +15 -29
- package/dist/prompts/slider.js +6 -5
- package/dist/prompts/text.d.ts +2 -1
- package/dist/prompts/text.js +198 -122
- package/dist/prompts/toggle.js +2 -6
- package/dist/utils.d.ts +23 -0
- package/dist/utils.js +135 -0
- package/package.json +4 -4
package/dist/base.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { detectCapabilities } from './utils';
|
|
1
2
|
/**
|
|
2
3
|
* Abstract base class for all prompts.
|
|
3
4
|
* Handles common logic like stdin management, raw mode, and cleanup
|
|
@@ -10,7 +11,12 @@ export declare abstract class Prompt<T, O> {
|
|
|
10
11
|
protected stdout: NodeJS.WriteStream;
|
|
11
12
|
private _resolve?;
|
|
12
13
|
private _reject?;
|
|
14
|
+
private _inputParser;
|
|
15
|
+
private _onKeyHandler?;
|
|
13
16
|
private _onDataHandler?;
|
|
17
|
+
protected lastRenderHeight: number;
|
|
18
|
+
protected lastRenderLines: string[];
|
|
19
|
+
protected capabilities: ReturnType<typeof detectCapabilities>;
|
|
14
20
|
constructor(options: O);
|
|
15
21
|
/**
|
|
16
22
|
* Renders the UI. Must be implemented by subclasses.
|
|
@@ -37,6 +43,13 @@ export declare abstract class Prompt<T, O> {
|
|
|
37
43
|
* Submits the final value and resolves the promise.
|
|
38
44
|
*/
|
|
39
45
|
protected submit(result: T): void;
|
|
46
|
+
/**
|
|
47
|
+
* Render Method with Diffing (Virtual DOM for CLI).
|
|
48
|
+
* Calculates new lines, compares with old lines, and updates only changed parts.
|
|
49
|
+
*/
|
|
50
|
+
protected renderFrame(content: string): void;
|
|
51
|
+
protected stripAnsi(str: string): string;
|
|
52
|
+
protected truncate(str: string, width: number): string;
|
|
40
53
|
protected isUp(char: string): boolean;
|
|
41
54
|
protected isDown(char: string): boolean;
|
|
42
55
|
protected isRight(char: string): boolean;
|
package/dist/base.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Prompt = void 0;
|
|
4
4
|
const ansi_1 = require("./ansi");
|
|
5
|
+
const input_1 = require("./input");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
5
7
|
/**
|
|
6
8
|
* Abstract base class for all prompts.
|
|
7
9
|
* Handles common logic like stdin management, raw mode, and cleanup
|
|
@@ -9,9 +11,14 @@ const ansi_1 = require("./ansi");
|
|
|
9
11
|
*/
|
|
10
12
|
class Prompt {
|
|
11
13
|
constructor(options) {
|
|
14
|
+
// Smart Cursor State
|
|
15
|
+
this.lastRenderHeight = 0;
|
|
16
|
+
this.lastRenderLines = [];
|
|
12
17
|
this.options = options;
|
|
13
18
|
this.stdin = process.stdin;
|
|
14
19
|
this.stdout = process.stdout;
|
|
20
|
+
this._inputParser = new input_1.InputParser();
|
|
21
|
+
this.capabilities = (0, utils_1.detectCapabilities)();
|
|
15
22
|
}
|
|
16
23
|
print(text) {
|
|
17
24
|
this.stdout.write(text);
|
|
@@ -31,10 +38,14 @@ class Prompt {
|
|
|
31
38
|
this.stdin.setEncoding('utf8');
|
|
32
39
|
// Initial render: Default to hidden cursor (good for menus)
|
|
33
40
|
// Subclasses like TextPrompt will explicitly show it if needed.
|
|
41
|
+
if (this.capabilities.isCI) {
|
|
42
|
+
// In CI, maybe don't hide cursor or do nothing?
|
|
43
|
+
// But for now follow standard flow.
|
|
44
|
+
}
|
|
34
45
|
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
35
46
|
this.render(true);
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
// Setup Input Parser Listeners
|
|
48
|
+
this._onKeyHandler = (char, buffer) => {
|
|
38
49
|
// Global Exit Handler (Ctrl+C)
|
|
39
50
|
if (char === '\u0003') {
|
|
40
51
|
this.cleanup();
|
|
@@ -45,6 +56,10 @@ class Prompt {
|
|
|
45
56
|
}
|
|
46
57
|
this.handleInput(char, buffer);
|
|
47
58
|
};
|
|
59
|
+
this._inputParser.on('keypress', this._onKeyHandler);
|
|
60
|
+
this._onDataHandler = (buffer) => {
|
|
61
|
+
this._inputParser.feed(buffer);
|
|
62
|
+
};
|
|
48
63
|
this.stdin.on('data', this._onDataHandler);
|
|
49
64
|
});
|
|
50
65
|
}
|
|
@@ -55,6 +70,9 @@ class Prompt {
|
|
|
55
70
|
if (this._onDataHandler) {
|
|
56
71
|
this.stdin.removeListener('data', this._onDataHandler);
|
|
57
72
|
}
|
|
73
|
+
if (this._onKeyHandler) {
|
|
74
|
+
this._inputParser.removeListener('keypress', this._onKeyHandler);
|
|
75
|
+
}
|
|
58
76
|
if (typeof this.stdin.setRawMode === 'function') {
|
|
59
77
|
this.stdin.setRawMode(false);
|
|
60
78
|
}
|
|
@@ -70,6 +88,242 @@ class Prompt {
|
|
|
70
88
|
if (this._resolve)
|
|
71
89
|
this._resolve(result);
|
|
72
90
|
}
|
|
91
|
+
// --- Rendering Utilities ---
|
|
92
|
+
/**
|
|
93
|
+
* Render Method with Diffing (Virtual DOM for CLI).
|
|
94
|
+
* Calculates new lines, compares with old lines, and updates only changed parts.
|
|
95
|
+
*/
|
|
96
|
+
renderFrame(content) {
|
|
97
|
+
// Ensure lines are truncated to terminal width
|
|
98
|
+
const width = this.stdout.columns || 80;
|
|
99
|
+
const rawLines = content.split('\n');
|
|
100
|
+
// Truncate each line and prepare the new buffer
|
|
101
|
+
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
|
+
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`);
|
|
115
|
+
}
|
|
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].
|
|
120
|
+
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
|
|
142
|
+
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
|
+
this.print('\n');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
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
|
|
260
|
+
this.lastRenderLines = newLines;
|
|
261
|
+
this.lastRenderHeight = newLines.length;
|
|
262
|
+
}
|
|
263
|
+
stripAnsi(str) {
|
|
264
|
+
return (0, utils_1.stripAnsi)(str);
|
|
265
|
+
}
|
|
266
|
+
truncate(str, width) {
|
|
267
|
+
const visualWidth = (0, utils_1.stringWidth)(str);
|
|
268
|
+
if (visualWidth <= width) {
|
|
269
|
+
return str;
|
|
270
|
+
}
|
|
271
|
+
// Heuristic truncation using stringWidth
|
|
272
|
+
// We iterate and sum width until we hit limit - 3
|
|
273
|
+
let currentWidth = 0;
|
|
274
|
+
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
|
+
let inAnsi = false;
|
|
280
|
+
for (let i = 0; i < str.length; i++) {
|
|
281
|
+
const code = str.charCodeAt(i);
|
|
282
|
+
if (str[i] === '\x1b') {
|
|
283
|
+
inAnsi = true;
|
|
284
|
+
}
|
|
285
|
+
if (inAnsi) {
|
|
286
|
+
if ((str[i] >= '@' && str[i] <= '~') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) {
|
|
287
|
+
inAnsi = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Width check
|
|
292
|
+
// Handle surrogates roughly (we don't need perfect width here during loop, just enough to stop)
|
|
293
|
+
// But wait, we imported `stringWidth`.
|
|
294
|
+
// We can't easily use `stringWidth` incrementally without re-parsing.
|
|
295
|
+
// Let's just trust the loop for cut index.
|
|
296
|
+
// Re-implement basic width logic here for the cut index finding
|
|
297
|
+
let charWidth = 1;
|
|
298
|
+
let cp = code;
|
|
299
|
+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
|
|
300
|
+
const next = str.charCodeAt(i + 1);
|
|
301
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
302
|
+
cp = (code - 0xD800) * 0x400 + (next - 0xDC00) + 0x10000;
|
|
303
|
+
// i is incremented in main loop but we need to skip next char
|
|
304
|
+
// We'll handle i increment in the loop
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Check range (simplified or call helper)
|
|
308
|
+
// We don't have isWideCodePoint exported.
|
|
309
|
+
// But generally, we can just say:
|
|
310
|
+
if (cp >= 0x1100) { // Quick check for potentially wide
|
|
311
|
+
// It's acceptable to be slightly aggressive on wide chars for truncation
|
|
312
|
+
charWidth = 2;
|
|
313
|
+
}
|
|
314
|
+
if (currentWidth + charWidth > width - 3) {
|
|
315
|
+
cutIndex = i;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
currentWidth += charWidth;
|
|
319
|
+
if (cp > 0xFFFF) {
|
|
320
|
+
i++; // Skip low surrogate
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
cutIndex = i + 1;
|
|
324
|
+
}
|
|
325
|
+
return str.substring(0, cutIndex) + '...' + ansi_1.ANSI.RESET;
|
|
326
|
+
}
|
|
73
327
|
// Helper to check for arrow keys including application mode
|
|
74
328
|
isUp(char) {
|
|
75
329
|
return char === '\u001b[A' || char === '\u001bOA';
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export declare class InputParser extends EventEmitter {
|
|
3
|
+
private buffer;
|
|
4
|
+
private timeout;
|
|
5
|
+
private state;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Feed data into the parser.
|
|
9
|
+
*/
|
|
10
|
+
feed(data: Buffer): void;
|
|
11
|
+
private processChar;
|
|
12
|
+
private emitKey;
|
|
13
|
+
}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/input.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.InputParser = void 0;
|
|
5
|
+
const events_1 = require("events");
|
|
6
|
+
class InputParser extends events_1.EventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.buffer = '';
|
|
10
|
+
this.timeout = null;
|
|
11
|
+
this.state = 'NORMAL';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Feed data into the parser.
|
|
15
|
+
*/
|
|
16
|
+
feed(data) {
|
|
17
|
+
// Convert buffer to string.
|
|
18
|
+
// For partial multi-byte sequences at the chunk boundary,
|
|
19
|
+
// buffer.toString() might produce replacement chars.
|
|
20
|
+
// Ideally we should use StringDecoder, but since we are handling KeyPresses,
|
|
21
|
+
// and usually a keypress is complete, simple toString often works.
|
|
22
|
+
// However, the user mentioned fragmentation issues.
|
|
23
|
+
// But InputParser usually receives data from stdin.
|
|
24
|
+
// The core issue of fragmentation is splitting escape codes like \x1b [ A
|
|
25
|
+
const input = data.toString('utf-8');
|
|
26
|
+
for (let i = 0; i < input.length; i++) {
|
|
27
|
+
const char = input[i];
|
|
28
|
+
this.processChar(char);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
processChar(char) {
|
|
32
|
+
if (this.state === 'NORMAL') {
|
|
33
|
+
if (char === '\x1b') {
|
|
34
|
+
this.state = 'ESC';
|
|
35
|
+
this.buffer = char;
|
|
36
|
+
// Start a short timeout to detect bare ESC
|
|
37
|
+
this.timeout = setTimeout(() => {
|
|
38
|
+
this.emitKey(this.buffer);
|
|
39
|
+
this.buffer = '';
|
|
40
|
+
this.state = 'NORMAL';
|
|
41
|
+
}, 20); // 20ms timeout
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.emitKey(char);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (this.state === 'ESC') {
|
|
48
|
+
// Cancel timeout as we received more data
|
|
49
|
+
if (this.timeout)
|
|
50
|
+
clearTimeout(this.timeout);
|
|
51
|
+
this.timeout = null;
|
|
52
|
+
this.buffer += char;
|
|
53
|
+
if (char === '[') {
|
|
54
|
+
this.state = 'CSI';
|
|
55
|
+
}
|
|
56
|
+
else if (char === 'O') {
|
|
57
|
+
// SS3 sequence like \x1b O A (Application Cursor Keys)
|
|
58
|
+
// Treat as similar to CSI for collecting the next char
|
|
59
|
+
this.state = 'CSI';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Alt + Key or similar (\x1b + char)
|
|
63
|
+
this.emitKey(this.buffer);
|
|
64
|
+
this.buffer = '';
|
|
65
|
+
this.state = 'NORMAL';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (this.state === 'CSI') {
|
|
69
|
+
this.buffer += char;
|
|
70
|
+
// CSI sequences end with 0x40-0x7E
|
|
71
|
+
if (char >= '@' && char <= '~') {
|
|
72
|
+
this.emitKey(this.buffer);
|
|
73
|
+
this.buffer = '';
|
|
74
|
+
this.state = 'NORMAL';
|
|
75
|
+
}
|
|
76
|
+
// Otherwise, we keep buffering (params like 1;2)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
emitKey(key) {
|
|
80
|
+
// Normalize Enter
|
|
81
|
+
if (key === '\r')
|
|
82
|
+
key = '\n';
|
|
83
|
+
// We emit both the raw sequence and a normalized representation if needed,
|
|
84
|
+
// but existing prompt logic handles raw strings like \x1b[A.
|
|
85
|
+
// So we just emit the reconstructed sequence.
|
|
86
|
+
this.emit('keypress', key, Buffer.from(key));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.InputParser = InputParser;
|
|
@@ -4,6 +4,8 @@ export declare class CheckboxPrompt extends Prompt<any[], CheckboxOptions> {
|
|
|
4
4
|
private selectedIndex;
|
|
5
5
|
private checkedState;
|
|
6
6
|
private errorMsg;
|
|
7
|
+
private scrollTop;
|
|
8
|
+
private readonly pageSize;
|
|
7
9
|
constructor(options: CheckboxOptions);
|
|
8
10
|
protected render(firstRender: boolean): void;
|
|
9
11
|
protected handleInput(char: string): void;
|
package/dist/prompts/checkbox.js
CHANGED
|
@@ -10,36 +10,55 @@ class CheckboxPrompt extends base_1.Prompt {
|
|
|
10
10
|
super(options);
|
|
11
11
|
this.selectedIndex = 0;
|
|
12
12
|
this.errorMsg = '';
|
|
13
|
+
// Pagination state (added for consistency and performance)
|
|
14
|
+
this.scrollTop = 0;
|
|
15
|
+
this.pageSize = 10;
|
|
13
16
|
this.checkedState = options.choices.map(c => !!c.selected);
|
|
14
17
|
}
|
|
15
18
|
render(firstRender) {
|
|
16
|
-
//
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
const extraLines = this.errorMsg ? 1 : 0;
|
|
20
|
-
this.print(`\x1b[${this.options.choices.length + 1 + extraLines}A`);
|
|
19
|
+
// Adjust Scroll Top
|
|
20
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
21
|
+
this.scrollTop = this.selectedIndex;
|
|
21
22
|
}
|
|
22
|
-
this.
|
|
23
|
+
else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
|
|
24
|
+
this.scrollTop = this.selectedIndex - this.pageSize + 1;
|
|
25
|
+
}
|
|
26
|
+
// Ensure we don't scroll past bounds if list is small
|
|
27
|
+
if (this.options.choices.length <= this.pageSize) {
|
|
28
|
+
this.scrollTop = 0;
|
|
29
|
+
}
|
|
30
|
+
let output = '';
|
|
31
|
+
// Header
|
|
23
32
|
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
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
|
+
// List
|
|
35
|
+
const choices = this.options.choices;
|
|
36
|
+
const visibleChoices = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
|
|
37
|
+
visibleChoices.forEach((choice, index) => {
|
|
38
|
+
const actualIndex = this.scrollTop + index;
|
|
39
|
+
output += '\n'; // New line for each item
|
|
40
|
+
const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
|
|
41
|
+
const isChecked = this.checkedState[actualIndex];
|
|
29
42
|
const checkbox = isChecked
|
|
30
43
|
? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
|
|
31
44
|
: `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
|
|
32
|
-
const title =
|
|
45
|
+
const title = actualIndex === this.selectedIndex
|
|
33
46
|
? `${theme_1.theme.main}${choice.title}${ansi_1.ANSI.RESET}`
|
|
34
47
|
: choice.title;
|
|
35
|
-
|
|
48
|
+
output += `${cursor} ${checkbox} ${title}`;
|
|
36
49
|
});
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
// Indication of more items
|
|
51
|
+
if (choices.length > this.pageSize) {
|
|
52
|
+
const progress = ` ${this.scrollTop + 1}-${Math.min(this.scrollTop + this.pageSize, choices.length)} of ${choices.length}`;
|
|
53
|
+
// Maybe add this to the header or footer?
|
|
54
|
+
// Let's add it to footer or header. Adding to header is cleaner.
|
|
55
|
+
// But I already wrote header.
|
|
56
|
+
// Let's just append it at the bottom if I want, or ignore for now to keep UI minimal.
|
|
39
57
|
}
|
|
40
|
-
|
|
41
|
-
this.
|
|
58
|
+
if (this.errorMsg) {
|
|
59
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
42
60
|
}
|
|
61
|
+
this.renderFrame(output);
|
|
43
62
|
}
|
|
44
63
|
handleInput(char) {
|
|
45
64
|
if (char === '\r' || char === '\n') {
|
|
@@ -56,7 +75,10 @@ class CheckboxPrompt extends base_1.Prompt {
|
|
|
56
75
|
return;
|
|
57
76
|
}
|
|
58
77
|
this.cleanup();
|
|
59
|
-
|
|
78
|
+
// renderFrame cleans up lines, but doesn't print the final state "persisted" if we want to show the result?
|
|
79
|
+
// Usually we clear the prompt or show a summary.
|
|
80
|
+
// MepCLI seems to submit and let the caller decide or just print newline.
|
|
81
|
+
// Base `submit` prints newline.
|
|
60
82
|
const results = this.options.choices
|
|
61
83
|
.filter((_, i) => this.checkedState[i])
|
|
62
84
|
.map(c => c.value);
|
package/dist/prompts/confirm.js
CHANGED
|
@@ -11,15 +11,12 @@ class ConfirmPrompt extends base_1.Prompt {
|
|
|
11
11
|
this.value = options.initial ?? true;
|
|
12
12
|
}
|
|
13
13
|
render(firstRender) {
|
|
14
|
-
//
|
|
15
|
-
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
16
|
-
if (!firstRender) {
|
|
17
|
-
this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
|
|
18
|
-
}
|
|
14
|
+
// Prepare content
|
|
19
15
|
const hint = this.value ? `${ansi_1.ANSI.BOLD}Yes${ansi_1.ANSI.RESET}/no` : `yes/${ansi_1.ANSI.BOLD}No${ansi_1.ANSI.RESET}`;
|
|
20
|
-
|
|
16
|
+
let output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${theme_1.theme.muted}(${hint})${ansi_1.ANSI.RESET} `;
|
|
21
17
|
const text = this.value ? 'Yes' : 'No';
|
|
22
|
-
|
|
18
|
+
output += `${theme_1.theme.main}${text}${ansi_1.ANSI.RESET}`;
|
|
19
|
+
this.renderFrame(output);
|
|
23
20
|
}
|
|
24
21
|
handleInput(char) {
|
|
25
22
|
const c = char.toLowerCase();
|
|
@@ -35,6 +32,11 @@ class ConfirmPrompt extends base_1.Prompt {
|
|
|
35
32
|
this.value = false;
|
|
36
33
|
this.render(false);
|
|
37
34
|
}
|
|
35
|
+
// Allow left/right to toggle as well for better UX
|
|
36
|
+
if (this.isLeft(char) || this.isRight(char)) {
|
|
37
|
+
this.value = !this.value;
|
|
38
|
+
this.render(false);
|
|
39
|
+
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
exports.ConfirmPrompt = ConfirmPrompt;
|
package/dist/prompts/date.js
CHANGED
|
@@ -14,13 +14,7 @@ class DatePrompt extends base_1.Prompt {
|
|
|
14
14
|
this.value = options.initial || new Date();
|
|
15
15
|
}
|
|
16
16
|
render(firstRender) {
|
|
17
|
-
|
|
18
|
-
if (!firstRender) {
|
|
19
|
-
this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
|
|
20
|
-
if (this.errorMsg) {
|
|
21
|
-
this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
17
|
+
// Date formatting
|
|
24
18
|
const y = this.value.getFullYear();
|
|
25
19
|
const m = (this.value.getMonth() + 1).toString().padStart(2, '0');
|
|
26
20
|
const d = this.value.getDate().toString().padStart(2, '0');
|
|
@@ -34,14 +28,11 @@ class DatePrompt extends base_1.Prompt {
|
|
|
34
28
|
});
|
|
35
29
|
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
36
30
|
const dateStr = `${display[0]}-${display[1]}-${display[2]} ${display[3]}:${display[4]}`;
|
|
37
|
-
|
|
31
|
+
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}`;
|
|
38
32
|
if (this.errorMsg) {
|
|
39
|
-
|
|
40
|
-
this.print(ansi_1.ANSI.UP);
|
|
41
|
-
// restore cursor pos logic isn't needed as we are on one line mostly, but for consistency:
|
|
42
|
-
const promptLen = this.options.message.length + 3; // roughly
|
|
43
|
-
this.print(`\x1b[1000D\x1b[${promptLen + 15}C`); // approx move back
|
|
33
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
44
34
|
}
|
|
35
|
+
this.renderFrame(output);
|
|
45
36
|
}
|
|
46
37
|
handleInput(char) {
|
|
47
38
|
if (char === '\r' || char === '\n') {
|
|
@@ -66,20 +57,11 @@ class DatePrompt extends base_1.Prompt {
|
|
|
66
57
|
if (/^\d$/.test(char)) {
|
|
67
58
|
const maxLen = this.selectedField === 0 ? 4 : 2;
|
|
68
59
|
let nextBuffer = this.inputBuffer + char;
|
|
69
|
-
// If we exceed max length, reset to just the new char (assuming user is starting a new number)
|
|
70
|
-
// Or better: try to parse.
|
|
71
|
-
// Logic:
|
|
72
|
-
// 1. Try appending.
|
|
73
|
-
// 2. Validate.
|
|
74
|
-
// 3. If valid, keep.
|
|
75
|
-
// 4. If invalid (e.g. Month 13), assume new start -> set buffer to just char.
|
|
76
|
-
// However, we must respect field limits first.
|
|
77
60
|
if (nextBuffer.length > maxLen) {
|
|
78
61
|
nextBuffer = char;
|
|
79
62
|
}
|
|
80
63
|
const val = parseInt(nextBuffer, 10);
|
|
81
64
|
let valid = true;
|
|
82
|
-
// Pre-validation to decide if we should append or reset
|
|
83
65
|
if (this.selectedField === 1 && (val < 1 || val > 12))
|
|
84
66
|
valid = false; // Month
|
|
85
67
|
if (this.selectedField === 2 && (val < 1 || val > 31))
|
|
@@ -89,15 +71,12 @@ class DatePrompt extends base_1.Prompt {
|
|
|
89
71
|
if (this.selectedField === 4 && (val > 59))
|
|
90
72
|
valid = false; // Minute
|
|
91
73
|
if (!valid) {
|
|
92
|
-
// If appending made it invalid (e.g. was '1', typed '3' -> '13' invalid month),
|
|
93
|
-
// treat '3' as the start of a new number.
|
|
94
74
|
nextBuffer = char;
|
|
95
75
|
}
|
|
96
76
|
this.inputBuffer = nextBuffer;
|
|
97
77
|
const finalVal = parseInt(this.inputBuffer, 10);
|
|
98
78
|
const d = new Date(this.value);
|
|
99
79
|
if (this.selectedField === 0) {
|
|
100
|
-
// Year is special, we just set it.
|
|
101
80
|
d.setFullYear(finalVal);
|
|
102
81
|
}
|
|
103
82
|
else if (this.selectedField === 1)
|